#1295 Edges between subgraphs

This commit is contained in:
Knut Sveidqvist
2020-03-14 17:38:35 +01:00
parent 5f5e453fb3
commit 963a0ce6ef
9 changed files with 296 additions and 35 deletions

View File

@@ -14,6 +14,9 @@
.arrowheadPath {fill: red;}
.edgePath .path {stroke: red;}
.mermaid2 {
display: none;
}
</style>
</head>
<body>
@@ -33,14 +36,72 @@
C --> D
C --> D
</div>
<div class="mermaid" style="width: 100%; height: 100%">
flowchart LR
G-->H
G-->H
<div class="mermaid2" style="width: 100%; height: 100%; display: none">
graph LR
a --> b
subgraph id1 [Test]
H
a --apa--> c
b
c-->b
b-->H
end
G-->H
G-->c
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart TB
a --> b
subgraph id1 [Test]
a --apa--> c
b
c-->b
b-->H
end
G-->H
G-->id1
id1 --> I
I --> G
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart RL
a --> b
subgraph id1 [Test]
a --apa--> c
b
c-->b
b-->H
end
G-->H
G-->id1
id1 --> I
I --> G
</div>
<div class="mermaid2" style="width: 100%; height: 100%">
flowchart RL
subgraph id1 [Test]
a
end
b-->id1
</div>
<div class="mermaid" style="width: 100%; height: 100%">
flowchart RL
subgraph id1 [Test1]
a
end
subgraph id2 [Test2]
b
end
a --> id2
a --> b
b-->id1
id1 --> id2
</div>
</div>
<script src="./mermaid.js"></script>
<script>
@@ -53,7 +114,8 @@
// gantt: { axisFormat: '%m/%d/%Y' },
sequence: { actorMargin: 50, showSequenceNumbers: true },
// sequenceDiagram: { actorMargin: 300 } // deprecated
fontFamily: '"arial", sans-serif'
fontFamily: '"arial", sans-serif',
curve: 'linear',
});
</script>
</script>

View File

@@ -32,7 +32,7 @@ const rect = (parent, node) => {
.attr('width', node.width + padding)
.attr('height', node.height + padding);
logger.info('bbox', bbox.width, node.x, node.width);
// logger.info('bbox', bbox.width, node.x, node.width);
// Center the label
// label.attr('transform', 'translate(' + adj + ', ' + (node.y - node.height / 2) + ')');
label.attr(
@@ -57,7 +57,7 @@ const rect = (parent, node) => {
const shapes = { rect };
const clusterElems = {};
let clusterElems = {};
export const insertCluster = (elem, node) => {
clusterElems[node.id] = shapes[node.shape](elem, node);
@@ -70,6 +70,10 @@ export const getClusterTitleWidth = (elem, node) => {
return width;
};
export const clear = () => {
clusterElems = {};
};
export const positionCluster = node => {
const el = clusterElems[node.id];
el.attr('transform', 'translate(' + node.x + ', ' + node.y + ')');

View File

@@ -1,9 +1,14 @@
import { logger } from '../logger'; // eslint-disable-line
import createLabel from './createLabel';
import * as d3 from 'd3';
import inter from './intersect/index.js';
import { getConfig } from '../config';
const edgeLabels = {};
let edgeLabels = {};
export const clear = () => {
edgeLabels = {};
};
export const insertEdgeLabel = (elem, edge) => {
// Create the actual text element
@@ -30,7 +35,6 @@ export const insertEdgeLabel = (elem, edge) => {
export const positionEdgeLabel = edge => {
const el = edgeLabels[edge.id];
logger.info(edge.id, el);
el.attr('transform', 'translate(' + edge.x + ', ' + edge.y + ')');
};
@@ -47,9 +51,128 @@ export const positionEdgeLabel = edge => {
// }
// };
export const insertEdge = function(elem, edge) {
const outsideNode = (node, point) => {
const x = node.x;
const y = node.y;
const dx = Math.abs(point.x - x);
const dy = Math.abs(point.y - y);
const w = node.width / 2;
const h = node.height / 2;
if (dx > w || dy > h) {
return true;
}
return false;
};
// const intersection = (node, outsidePoint, insidePoint) => {
// const x = node.x;
// const y = node.y;
// const dx = Math.abs(x - insidePoint.x);
// const w = node.width / 2;
// let r = w - dx;
// const dy = Math.abs(y - insidePoint.y);
// const h = node.height / 2;
// const q = h - dy;
// const Q = Math.abs(outsidePoint.y - insidePoint.y);
// const R = Math.abs(outsidePoint.x - insidePoint.x);
// r = (R * q) / Q;
// return { x: insidePoint.x + r, y: insidePoint.y + q };
// };
const intersection = (node, outsidePoint, insidePoint) => {
const x = node.x;
const y = node.y;
const dx = Math.abs(x - insidePoint.x);
const w = node.width / 2;
let r = w - dx;
const dy = Math.abs(y - insidePoint.y);
const h = node.height / 2;
let q = h - dy;
logger.info('q och r', q, r);
const Q = Math.abs(outsidePoint.y - insidePoint.y);
const R = Math.abs(outsidePoint.x - insidePoint.x);
// if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h || false) { // eslint-disable-line
// // Intersection is top or bottom of rect.
// r = (R * q) / Q;
// return {
// x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
// y: insidePoint.y + q
// };
// } else {
q = (Q * r) / R;
return {
x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - r,
y: insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q
};
// }
};
export const insertEdge = function(elem, edge, clusterDb) {
let points = edge.points;
if (edge.toCluster) {
logger.trace('edge', edge);
logger.trace('cluster', clusterDb[edge.toCluster]);
points = [];
let lastPointOutside;
let isInside = false;
edge.points.forEach(point => {
const node = clusterDb[edge.toCluster].node;
if (!outsideNode(node, point) && !isInside) {
logger.info('inside', edge.toCluster, point);
// First point inside the rect
const insterection = intersection(node, lastPointOutside, point);
logger.info('intersect', inter.rect(node, lastPointOutside));
points.push(insterection);
// points.push(insterection);
isInside = true;
} else {
if (!isInside) points.push(point);
}
lastPointOutside = point;
});
}
if (edge.fromCluster) {
logger.info('edge', edge);
logger.info('cluster', clusterDb[edge.toCluster]);
const updatedPoints = [];
let lastPointOutside;
let isInside = false;
for (let i = points.length - 1; i >= 0; i--) {
const point = points[i];
const node = clusterDb[edge.fromCluster].node;
if (!outsideNode(node, point) && !isInside) {
logger.info('inside', edge.toCluster, point);
// First point inside the rect
const insterection = intersection(node, lastPointOutside, point);
logger.info('intersect', inter.rect(node, lastPointOutside));
updatedPoints.unshift(insterection);
// points.push(insterection);
isInside = true;
} else {
if (!isInside) updatedPoints.unshift(point);
}
lastPointOutside = point;
}
points = updatedPoints;
}
logger.info('Points', points);
// The data for our line
const lineData = edge.points.filter(p => !Number.isNaN(p.y));
const lineData = points.filter(p => !Number.isNaN(p.y));
// This is the accessor function we talked about above
const lineFunction = d3
@@ -59,14 +182,25 @@ export const insertEdge = function(elem, edge) {
})
.y(function(d) {
return d.y;
})
.curve(d3.curveBasis);
});
// .curve(d3.curveBasis);
const svgPath = elem
.append('path')
.attr('d', lineFunction(lineData))
.attr('id', edge.id)
.attr('class', 'transition');
// edge.points.forEach(point => {
// elem
// .append('circle')
// .style('stroke', 'red')
// .style('fill', 'red')
// .attr('r', 1)
// .attr('cx', point.x)
// .attr('cy', point.y);
// });
let url = '';
if (getConfig().state.arrowMarkerAbsolute) {
url =
@@ -79,6 +213,6 @@ export const insertEdge = function(elem, edge) {
url = url.replace(/\)/g, '\\)');
}
svgPath.attr('marker-end', 'url(' + url + '#' + 'extensionEnd' + ')');
svgPath.attr('marker-start', 'url(' + url + '#' + 'extensionStart' + ')');
svgPath.attr('marker-end', 'url(' + url + '#' + 'normalEnd' + ')');
// svgPath.attr('marker-start', 'url(' + url + '#' + 'normalStart' + ')');
};

View File

@@ -1,12 +1,23 @@
import dagre from 'dagre';
import insertMarkers from './markers';
import { insertNode, positionNode } from './nodes';
import { insertCluster } from './clusters';
import { insertEdgeLabel, positionEdgeLabel, insertEdge } from './edges';
import { insertNode, positionNode, clearNodes } from './nodes';
import { insertCluster, clearClusters } from './clusters';
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clearEdges } from './edges';
import { logger } from '../logger';
let clusterDb = {};
const translateClusterId = id => {
if (clusterDb[id]) return clusterDb[id].id;
return id;
};
export const render = (elem, graph) => {
insertMarkers(elem);
clusterDb = {};
clearNodes();
clearEdges();
clearClusters();
const clusters = elem.insert('g').attr('class', 'clusters'); // eslint-disable-line
const edgePaths = elem.insert('g').attr('class', 'edgePaths');
@@ -17,27 +28,41 @@ export const render = (elem, graph) => {
// to the abstract node and is later used by dagre for the layout
graph.nodes().forEach(function(v) {
const node = graph.node(v);
logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
logger.trace('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
if (node.type !== 'group') {
insertNode(nodes, graph.node(v));
} else {
// const width = getClusterTitleWidth(clusters, node);
// const children = graph.children(v);
const children = graph.children(v);
logger.info('Cluster identified', node.id, children[0]);
// nodes2expand.push({ id: children[0], width });
clusterDb[node.id] = { id: children[0] };
logger.info('Clusters ', clusterDb);
}
});
// nodes2expand.forEach(item => {
// const node = graph.node(item.id);
// node.width = item.width;
// });
// Inster labels, this will insert them into the dom so that the width can be calculated
// 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
// Edges from/to clusters really points to the first child in the cluster.
// TODO: pick optimal child in the cluster to us as link anchor
graph.edges().forEach(function(e) {
logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
insertEdgeLabel(edgeLabels, graph.edge(e));
const edge = graph.edge(e);
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(graph.edge(e)));
const v = translateClusterId(e.v);
const w = translateClusterId(e.w);
if (v !== e.v || w !== e.w) {
graph.removeEdge(e.v, e.w, e.name);
if (v !== e.v) edge.fromCluster = e.v;
if (w !== e.w) edge.toCluster = e.w;
graph.setEdge(v, w, edge, e.name);
}
insertEdgeLabel(edgeLabels, edge);
});
// graph.edges().forEach(function(e) {
// logger.info('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(e));
// });
logger.info('#############################################');
logger.info('### Layout ###');
logger.info('#############################################');
@@ -46,11 +71,12 @@ export const render = (elem, graph) => {
// Move the nodes to the correct place
graph.nodes().forEach(function(v) {
const node = graph.node(v);
logger.info('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
logger.trace('Node ' + v + ': ' + JSON.stringify(graph.node(v)));
if (node.type !== 'group') {
positionNode(node);
} else {
insertCluster(clusters, node);
clusterDb[node.id].node = node;
}
});
@@ -59,7 +85,7 @@ export const render = (elem, graph) => {
const edge = graph.edge(e);
logger.trace('Edge ' + e.v + ' -> ' + e.w + ': ' + JSON.stringify(edge));
insertEdge(edgePaths, edge);
insertEdge(edgePaths, edge, clusterDb);
positionEdgeLabel(edge);
});
};

View File

@@ -8,7 +8,7 @@ import ellipse from './intersect-ellipse';
import polygon from './intersect-polygon';
import rect from './intersect-rect';
module.exports = {
export default {
node,
circle,
ellipse,

View File

@@ -2,6 +2,7 @@ const intersectRect = (node, point) => {
var x = node.x;
var y = node.y;
console.log(node, point);
// Rectangle intersection algorithm from:
// http://math.stackexchange.com/questions/108113/find-edge-between-two-boxes
var dx = point.x - x;

View File

@@ -101,6 +101,37 @@ const insertMarkers = elem => {
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 18,7 L9,13 L14,7 L9,1 Z');
elem
.append('marker')
.attr('id', 'normalEnd')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 5)
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
.attr('class', 'arrowheadPath')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
elem
.append('marker')
.attr('id', 'normalStart')
.attr('viewBox', '0 0 10 10')
.attr('refX', 9)
.attr('refY', 5)
.attr('markerUnits', 'strokeWidth')
.attr('markerWidth', 8)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 0 0 L 10 5 L 0 10 z')
.attr('class', 'arrowheadPath')
.style('stroke-width', 1)
.style('stroke-dasharray', '1,0');
};
export default insertMarkers;

View File

@@ -47,11 +47,14 @@ const rect = (parent, node) => {
const shapes = { rect };
const nodeElems = {};
let nodeElems = {};
export const insertNode = (elem, node) => {
nodeElems[node.id] = shapes[node.shape](elem, node);
};
export const clear = () => {
nodeElems = {};
};
export const positionNode = node => {
const el = nodeElems[node.id];

View File

@@ -1,5 +1,5 @@
import * as d3 from 'd3';
import { logger } from '../../logger';
import { logger } from '../../logger'; // eslint-disable-line
import utils from '../../utils';
import { getConfig } from '../../config';
import common from '../common/common';
@@ -88,7 +88,7 @@ export const addSingleLink = function(_start, _end, type, linktext) {
let end = _end;
if (start[0].match(/\d/)) start = MERMAID_DOM_ID_PREFIX + start;
if (end[0].match(/\d/)) end = MERMAID_DOM_ID_PREFIX + end;
logger.info('Got edge...', start, end);
// logger.info('Got edge...', start, end);
const edge = { start: start, end: end, type: undefined, text: '' };
linktext = type.text;