Added dagre to handle our workflow graph layout. Fixed various workflow related bugs.

This commit is contained in:
mabashian 2018-11-13 15:12:13 -05:00
parent b81d795c00
commit ae0d0db62c
5 changed files with 233 additions and 109 deletions

View File

@ -23,8 +23,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
restrict: 'E',
link: function(scope, element) {
let marginLeft = 20,
nodeW = 180,
let nodeW = 180,
nodeH = 60,
rootW = 60,
rootH = 40,
@ -32,11 +31,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
maxNodeTextLength = 27,
windowHeight,
windowWidth,
line,
zoomObj,
baseSvg,
svgGroup,
graphLoaded,
force;
nodePositionMap = {};
scope.dimensionsSet = false;
@ -54,16 +54,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
});
function init() {
force = d3.layout.force()
// .gravity(0)
// .linkStrength(2)
// .friction(0.4)
// .charge(-4000)
// .linkDistance(300)
.gravity(0)
.charge(-300)
.linkDistance(300)
.size([windowHeight, windowWidth]);
line = d3.svg.line()
.x(function (d) {
return d.x;
})
.y(function (d) {
return d.y;
});
zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]);
@ -75,7 +72,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
svgGroup = baseSvg.append("g")
.attr("id", "aw-workflow-chart-g")
.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
.attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")");
}
function calcAvailableScreenSpace() {
@ -102,6 +99,37 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return dimensions;
}
// Dagre is going to shift the root node around as nodes are added/removed
// This function ensures that the user doesn't experience that
let normalizeY = ((y) => {
return y - nodePositionMap[1].y;
});
function lineData(d) {
let sourceX = nodePositionMap[d.source.id].x + (nodePositionMap[d.source.id].width);
let sourceY = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
let targetX = nodePositionMap[d.target.id].x;
let targetY = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
// There's something off with the math on the root node...
if (d.source.id === 1) {
sourceY = sourceY + 10;
}
let points = [{
x: sourceX,
y: sourceY
},
{
x: targetX,
y: targetY
}
];
return line(points);
}
// TODO: this function is hacky and we need to come up with a better solution
// see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
function wrap(text) {
@ -137,7 +165,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
let scale = d3.event.scale,
translation = d3.event.translate;
translation = [translation[0] + (marginLeft*scale), translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
translation = [translation[0], translation[1] + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)];
svgGroup.attr("transform", "translate(" + translation + ")scale(" + scale + ")");
@ -156,7 +184,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/2;
svgGroup.attr("transform", "translate(" + [translateX + (marginLeft*scale), translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
svgGroup.attr("transform", "translate(" + [translateX, translateY + ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scale)] + ")scale(" + scale + ")");
zoomObj.scale(scale);
zoomObj.translate([translateX, translateY]);
}
@ -179,7 +207,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}
function resetZoomAndPan() {
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
svgGroup.attr("transform", "translate(0," + (windowHeight/2 - rootH/2 - startNodeOffsetY) + ")scale(" + 1 + ")");
// Update the zoomObj
zoomObj.scale(1);
zoomObj.translate([0,0]);
@ -187,16 +215,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function zoomToFitChart() {
let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
availableScreenSpace = calcAvailableScreenSpace(),
currentZoomValue = zoomObj.scale(),
unscaledH = graphDimensions.height/currentZoomValue,
unscaledW = graphDimensions.width/currentZoomValue,
scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft)/unscaledW,
scaleNeededForMaxWidth = (availableScreenSpace.width)/unscaledW,
lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10),
startNodeOffsetFromGraphCenter = Math.round((((rootH/2) + (startNodeDimensions.top/currentZoomValue)) - ((graphDimensions.top/currentZoomValue) + (unscaledH/2)))*scaleToFit);
scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10);
manualZoom(scaleToFit*100);
@ -204,12 +230,42 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
zoom: scaleToFit
});
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter) + ")scale(" + scaleToFit + ")");
zoomObj.translate([marginLeft - scaleToFit*marginLeft, windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
svgGroup.attr("transform", "translate(0," + (windowHeight/2 - (nodeH*scaleToFit/2)) + ")scale(" + scaleToFit + ")");
zoomObj.translate([0, windowHeight/2 - (nodeH*scaleToFit/2) - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
}
function update() {
if(scope.dimensionsSet) {
var g = new dagre.graphlib.Graph();
g.setGraph({rankdir: 'LR', nodesep: 30, ranksep: 120});
g.setDefaultEdgeLabel(function() { return {}; });
scope.graphState.arrayOfNodesForChart.forEach((node) => {
if (node.id === 1) {
if (scope.mode === "details") {
g.setNode(node.id, { label: "", width: 25, height: 25 });
} else {
g.setNode(node.id, { label: "", width: rootW, height: rootH });
}
} else {
g.setNode(node.id, { label: "", width: nodeW, height: nodeH });
}
});
scope.graphState.arrayOfLinksForChart.forEach((link) => {
g.setEdge(link.source.id, link.target.id);
});
dagre.layout(g);
nodePositionMap = {};
g.nodes().forEach((node) => {
nodePositionMap[node] = g.node(node);
});
let links = svgGroup.selectAll(".WorkflowChart-link")
.data(scope.graphState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; });
@ -221,9 +277,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
baseSvg.selectAll(".WorkflowChart-linkPath")
.transition()
.attr("class", function(d) {
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
})
.attr("d", lineData)
.attr('stroke', function(d) {
let edgeType = d.edgeType;
if(edgeType) {
@ -254,15 +312,64 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
linkClasses.push("WorkflowChart-link--active");
}
return linkClasses.join(' ');
})
.attr("points",function(d) {
let x1 = nodePositionMap[d.target.id].x;
let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width;
let y2 = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
let slope = (y2 - y1)/(x2-x1);
let yIntercept = y1 - slope*x1;
let orthogonalDistance = 8;
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
return [pt1, pt2, pt3, pt4].join(" ");
});
baseSvg.selectAll(".WorkflowChart-circleBetweenNodes")
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";})
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; });
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
.attr("cx", function(d) {
return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2;
})
.attr("cy", function(d) {
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
if (d.source.id === 1) {
yPos = yPos + 4;
}
return yPos;
});
baseSvg.selectAll(".WorkflowChart-betweenNodesIcon")
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; });
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
.attr("transform", function(d) {
let translate;
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
if (d.source.id === 1) {
yPos = yPos + 4;
}
translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")";
return translate;
});
// Add any new links
let linkEnter = links.enter().append("g")
@ -283,6 +390,22 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
})
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
.call(edit_link)
.attr("points",function(d) {
let x1 = nodePositionMap[d.target.id].x;
let y1 = normalizeY(nodePositionMap[d.target.id].y) + (nodePositionMap[d.target.id].height/2);
let x2 = nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].width;
let y2 = normalizeY(nodePositionMap[d.source.id].y) + (nodePositionMap[d.source.id].height/2);
let slope = (y2 - y1)/(x2-x1);
let yIntercept = y1 - slope*x1;
let orthogonalDistance = 8;
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
return [pt1, pt2, pt3, pt4].join(" ");
})
.on("mouseover", function(d) {
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
@ -290,13 +413,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
arrowClass = 'WorkflowChart-tooltipArrow--down';
} else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
arrowClass = 'WorkflowChart-tooltipArrow--right';
}
@ -337,10 +460,11 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
});
// Add entering links in the parents old position.
linkEnter.append("line")
linkEnter.insert("path", "g")
.attr("class", function(d) {
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath";
})
.attr("d", lineData)
.call(edit_link)
.on("mouseenter", function(d) {
if(!scope.graphState.isLinkMode && !d.source.isStartNode && d.source.id !== scope.graphState.nodeBeingAdded && d.target.id !== scope.graphState.nodeBeingAdded && scope.mode !== 'details') {
@ -349,13 +473,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2);
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100;
if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
arrowClass = 'WorkflowChart-tooltipArrow--down';
} else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50;
xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
arrowClass = 'WorkflowChart-tooltipArrow--right';
}
@ -416,6 +540,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.attr("r", 10)
.attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes")
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
.attr("cx", function(d) {
return (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2;
})
.attr("cy", function(d) {
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
if (d.source.id === 1) {
yPos = yPos + 4;
}
return yPos;
})
.call(add_node_with_child)
.on("mouseover", function(d) {
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
@ -436,6 +577,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.type("cross")
)
.style("display", function(d) { return (scope.graphState.isLinkMode || d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded || scope.readOnly) ? "none" : null; })
.attr("transform", function(d) {
let translate;
const normalizedSourceY = normalizeY(nodePositionMap[d.source.id].y);
const halfSourceHeight = nodePositionMap[d.source.id].height/2;
const normalizedTargetY = normalizeY(nodePositionMap[d.target.id].y);
const halfTargetHeight = nodePositionMap[d.target.id].height/2;
let yPos = (normalizedSourceY + halfSourceHeight + normalizedTargetY + halfTargetHeight)/2;
if (d.source.id === 1) {
yPos = yPos + 4;
}
translate = "translate(" + (nodePositionMap[d.source.id].x + nodePositionMap[d.source.id].width + nodePositionMap[d.target.id].x)/2 + "," + yPos + ")";
return translate;
})
.call(add_node_with_child)
.on("mouseover", function(d) {
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`);
@ -448,13 +606,6 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.classed("WorkflowChart-addHovering", false);
});
// Create references to all the link elements so that they can be transitioned
// properly in the tick function
let linkLines = svgGroup.selectAll(".WorkflowChart-link line");
let linkPolygons = svgGroup.selectAll(".WorkflowChart-link polygon");
let linkAddBetweenCircle = svgGroup.selectAll(".WorkflowChart-link circle");
let linkAddBetweenIcon = svgGroup.selectAll(".WorkflowChart-betweenNodesIcon");
let nodes = svgGroup.selectAll('.WorkflowChart-node')
.data(scope.graphState.arrayOfNodesForChart, function(d) { return d.id; });
@ -462,6 +613,15 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
nodes.exit().remove();
// Update existing nodes
baseSvg.selectAll(".WorkflowChart-node")
.transition()
.attr("transform", function (d) {
// Update prior x and prior y
d.px = d.x;
d.py = d.y;
return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")";
});
baseSvg.selectAll(".WorkflowChart-nodeAddCircle")
.style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; });
@ -640,7 +800,10 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.enter()
.append('g')
.attr("class", "WorkflowChart-node")
.attr("id", function(d){return "node-" + d.id;});
.attr("id", function(d){return "node-" + d.id;})
.attr("transform", function (d) {
return "translate(" + nodePositionMap[d.id].x + "," + normalizeY(nodePositionMap[d.id].y) + ")";
});
nodeEnter.each(function(d) {
let thisNode = d3.select(this);
@ -1064,75 +1227,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}
});
// TODO: this
// if(scope.graphState.arrayOfNodesForChart && scope.graphState.arrayOfNodesForChart > 1 && !graphLoaded) {
// zoomToFitChart();
// }
if(scope.graphState.arrayOfNodesForChart && scope.graphState.arrayOfNodesForChart.length > 1 && !graphLoaded) {
zoomToFitChart();
}
graphLoaded = true;
// This will make sure that all the link elements appear before the nodes in the dom
// TODO: i don't think this is working...
svgGroup.selectAll(".WorkflowChart-node").order();
let tick = () => {
linkLines
.each(function(d) {
d.target.y = scope.graphState.depthMap[d.target.id] * 300;
})
.attr("x1", function(d) { return d.target.y; })
.attr("y1", function(d) { return d.target.x + (nodeH/2); })
.attr("x2", function(d) { return d.source.index === 0 ? (scope.mode === 'details' ? d.source.y + 25 : d.source.y + 60) : (d.source.y + nodeW); })
.attr("y2", function(d) { return d.source.x + (nodeH/2); });
linkPolygons
.attr("points",function(d) {
let x1 = d.target.y;
let y1 = d.target.x + (nodeH/2);
let x2 = d.source.index === 0 ? (d.source.y + 60) : (d.source.y + nodeW);
let y2 = d.source.x + (nodeH/2);
let slope = (y2 - y1)/(x2-x1);
let yIntercept = y1 - slope*x1;
let orthogonalDistance = 8;
const pt1 = [x1, slope*x1 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt2 = [x2, slope*x2 + yIntercept + orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt3 = [x2, slope*x2 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
const pt4 = [x1, slope*x1 + yIntercept - orthogonalDistance*Math.sqrt(1+slope*slope)].join(",");
return [pt1, pt2, pt3, pt4].join(" ");
});
linkAddBetweenCircle
.attr("cx", function(d) {
return (d.source.isStartNode) ? (d.target.y + d.source.y + rootW) / 2 : (d.target.y + d.source.y + nodeW) / 2;
})
.attr("cy", function(d) {
return (d.source.isStartNode) ? ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 : (d.target.x + d.source.x + nodeH) / 2;
});
linkAddBetweenIcon
.attr("transform", function(d) {
let translate;
if(d.source.isStartNode) {
translate = "translate(" + (d.target.y + d.source.y + rootW) / 2 + "," + ((d.target.x + startNodeOffsetY + rootH/2) + (d.source.x + nodeH/2)) / 2 + ")";
}
else {
translate = "translate(" + (d.target.y + d.source.y + nodeW) / 2 + "," + (d.target.x + d.source.x + nodeH) / 2 + ")";
}
return translate;
});
nodes
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; });
};
force
.nodes(scope.graphState.arrayOfNodesForChart)
.links(scope.graphState.arrayOfLinksForChart)
.on("tick", tick)
.start();
}
else if(!scope.watchDimensionsSet){
scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
@ -1178,7 +1280,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function node_click() {
this.on("click", function(d) {
if(d.id !== scope.graphState.nodeBeingAdded && !scope.readOnly){
if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget) {
if(scope.graphState.isLinkMode && !d.isInvalidLinkTarget && scope.graphState.addLinkSource !== d.id) {
$('.WorkflowChart-potentialLink').remove();
scope.selectNodeForLinking({
nodeToStartLink: d

View File

@ -571,6 +571,9 @@ export default ['$scope', 'TemplatesService',
};
$scope.selectNodeForLinking = (node) => {
if ($scope.nodeConfig) {
$scope.cancelNodeForm();
}
if ($scope.linkConfig) {
// This is the second node selected
$scope.linkConfig.child = {

View File

@ -38,6 +38,7 @@ require('moment');
require('rrule');
require('sprintf-js');
require('reconnectingwebsocket');
global.dagre = require('dagre');
// D3 + extensions
require('d3');

View File

@ -3195,6 +3195,15 @@
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
"integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g="
},
"dagre": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.2.tgz",
"integrity": "sha512-TEOOGZOkCOgCG7AoUIq64sJ3d21SMv8tyoqteLpX+UsUsS9Qw8iap4hhogXY4oB3r0bbZuAjO0atAilgCmsE0Q==",
"requires": {
"graphlib": "^2.1.5",
"lodash": "^4.17.4"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -6185,6 +6194,14 @@
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
"dev": true
},
"graphlib": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.5.tgz",
"integrity": "sha512-XvtbqCcw+EM5SqQrIetIKKD+uZVNQtDPD1goIg7K73RuRZtVI5rYMdcCVSHm/AS1sCBZ7vt0p5WgXouucHQaOA==",
"requires": {
"lodash": "^4.11.1"
}
},
"growl": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",

View File

@ -117,6 +117,7 @@
"codemirror": "^5.17.0",
"components-font-awesome": "^4.6.1",
"d3": "^3.5.4",
"dagre": "^0.8.2",
"hamsterjs": "^1.1.2",
"html-entities": "^1.2.1",
"inherits": "^1.0.2",