From 963a0ce6efaffb7e8d48b1804f68a42b11ab039b Mon Sep 17 00:00:00 2001 From: Knut Sveidqvist Date: Sat, 14 Mar 2020 17:38:35 +0100 Subject: [PATCH] #1295 Edges between subgraphs --- cypress/platform/current.html | 74 ++++++++- src/dagre-wrapper/clusters.js | 8 +- src/dagre-wrapper/edges.js | 150 +++++++++++++++++- src/dagre-wrapper/index.js | 56 +++++-- src/dagre-wrapper/intersect/index.js | 2 +- src/dagre-wrapper/intersect/intersect-rect.js | 1 + src/dagre-wrapper/markers.js | 31 ++++ src/dagre-wrapper/nodes.js | 5 +- src/diagrams/flowchart/flowDb.js | 4 +- 9 files changed, 296 insertions(+), 35 deletions(-) diff --git a/cypress/platform/current.html b/cypress/platform/current.html index 0e151cdd0..0f5d69915 100644 --- a/cypress/platform/current.html +++ b/cypress/platform/current.html @@ -14,6 +14,9 @@ .arrowheadPath {fill: red;} .edgePath .path {stroke: red;} + .mermaid2 { + display: none; + } @@ -33,14 +36,72 @@ C --> D C --> D -
- flowchart LR - G-->H - G-->H + +
+ flowchart TB + a --> b + + subgraph id1 [Test] + a --apa--> c + b + c-->b + b-->H + end + G-->H + G-->id1 + id1 --> I + I --> G +
+
+ flowchart RL + a --> b + + subgraph id1 [Test] + a --apa--> c + b + c-->b + b-->H + end + G-->H + G-->id1 + id1 --> I + I --> G +
+
+ flowchart RL + + subgraph id1 [Test] + a + end + b-->id1 +
+
+ flowchart RL + + subgraph id1 [Test1] + a + end + subgraph id2 [Test2] + b + end + a --> id2 + a --> b + b-->id1 + id1 --> id2 +
+
diff --git a/src/dagre-wrapper/clusters.js b/src/dagre-wrapper/clusters.js index 74947b46d..4d0322192 100644 --- a/src/dagre-wrapper/clusters.js +++ b/src/dagre-wrapper/clusters.js @@ -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 + ')'); diff --git a/src/dagre-wrapper/edges.js b/src/dagre-wrapper/edges.js index 8ba6a64a2..c2135abf0 100644 --- a/src/dagre-wrapper/edges.js +++ b/src/dagre-wrapper/edges.js @@ -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' + ')'); }; diff --git a/src/dagre-wrapper/index.js b/src/dagre-wrapper/index.js index d2f12c491..1ad515236 100644 --- a/src/dagre-wrapper/index.js +++ b/src/dagre-wrapper/index.js @@ -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); }); }; diff --git a/src/dagre-wrapper/intersect/index.js b/src/dagre-wrapper/intersect/index.js index a68ea0bfa..5cf3a259c 100644 --- a/src/dagre-wrapper/intersect/index.js +++ b/src/dagre-wrapper/intersect/index.js @@ -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, diff --git a/src/dagre-wrapper/intersect/intersect-rect.js b/src/dagre-wrapper/intersect/intersect-rect.js index f8c03a2af..9847f9897 100644 --- a/src/dagre-wrapper/intersect/intersect-rect.js +++ b/src/dagre-wrapper/intersect/intersect-rect.js @@ -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; diff --git a/src/dagre-wrapper/markers.js b/src/dagre-wrapper/markers.js index d81babf0d..c9c6cd703 100644 --- a/src/dagre-wrapper/markers.js +++ b/src/dagre-wrapper/markers.js @@ -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; diff --git a/src/dagre-wrapper/nodes.js b/src/dagre-wrapper/nodes.js index 95d146da1..06e947dad 100644 --- a/src/dagre-wrapper/nodes.js +++ b/src/dagre-wrapper/nodes.js @@ -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]; diff --git a/src/diagrams/flowchart/flowDb.js b/src/diagrams/flowchart/flowDb.js index 4917a54a7..d6d766da6 100644 --- a/src/diagrams/flowchart/flowDb.js +++ b/src/diagrams/flowchart/flowDb.js @@ -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;