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', restrict: 'E',
link: function(scope, element) { link: function(scope, element) {
let marginLeft = 20, let nodeW = 180,
nodeW = 180,
nodeH = 60, nodeH = 60,
rootW = 60, rootW = 60,
rootH = 40, rootH = 40,
@@ -32,11 +31,12 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
maxNodeTextLength = 27, maxNodeTextLength = 27,
windowHeight, windowHeight,
windowWidth, windowWidth,
line,
zoomObj, zoomObj,
baseSvg, baseSvg,
svgGroup, svgGroup,
graphLoaded, graphLoaded,
force; nodePositionMap = {};
scope.dimensionsSet = false; scope.dimensionsSet = false;
@@ -54,16 +54,13 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
}); });
function init() { function init() {
force = d3.layout.force() line = d3.svg.line()
// .gravity(0) .x(function (d) {
// .linkStrength(2) return d.x;
// .friction(0.4) })
// .charge(-4000) .y(function (d) {
// .linkDistance(300) return d.y;
.gravity(0) });
.charge(-300)
.linkDistance(300)
.size([windowHeight, windowWidth]);
zoomObj = d3.behavior.zoom().scaleExtent([0.5, 2]); 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") svgGroup = baseSvg.append("g")
.attr("id", "aw-workflow-chart-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() { function calcAvailableScreenSpace() {
@@ -102,6 +99,37 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
return dimensions; 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 // 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 // see: http://stackoverflow.com/questions/15975440/add-ellipses-to-overflowing-text-in-svg#answer-27723752
function wrap(text) { function wrap(text) {
@@ -137,7 +165,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
let scale = d3.event.scale, let scale = d3.event.scale,
translation = d3.event.translate; 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 + ")"); 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, translateX = unscaledOffsetX*scale - ((scale*windowWidth)-windowWidth)/2,
translateY = unscaledOffsetY*scale - ((scale*windowHeight)-windowHeight)/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.scale(scale);
zoomObj.translate([translateX, translateY]); zoomObj.translate([translateX, translateY]);
} }
@@ -179,7 +207,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
} }
function resetZoomAndPan() { 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 // Update the zoomObj
zoomObj.scale(1); zoomObj.scale(1);
zoomObj.translate([0,0]); zoomObj.translate([0,0]);
@@ -187,16 +215,14 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function zoomToFitChart() { function zoomToFitChart() {
let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(), let graphDimensions = d3.select('#aw-workflow-chart-g')[0][0].getBoundingClientRect(),
startNodeDimensions = d3.select('.WorkflowChart-rootNode')[0][0].getBoundingClientRect(),
availableScreenSpace = calcAvailableScreenSpace(), availableScreenSpace = calcAvailableScreenSpace(),
currentZoomValue = zoomObj.scale(), currentZoomValue = zoomObj.scale(),
unscaledH = graphDimensions.height/currentZoomValue, unscaledH = graphDimensions.height/currentZoomValue,
unscaledW = graphDimensions.width/currentZoomValue, unscaledW = graphDimensions.width/currentZoomValue,
scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH, scaleNeededForMaxHeight = (availableScreenSpace.height)/unscaledH,
scaleNeededForMaxWidth = (availableScreenSpace.width - marginLeft)/unscaledW, scaleNeededForMaxWidth = (availableScreenSpace.width)/unscaledW,
lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth), lowerScale = Math.min(scaleNeededForMaxHeight, scaleNeededForMaxWidth),
scaleToFit = lowerScale < 0.5 ? 0.5 : (lowerScale > 2 ? 2 : Math.floor(lowerScale * 10)/10), 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);
manualZoom(scaleToFit*100); manualZoom(scaleToFit*100);
@@ -204,12 +230,42 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
zoom: scaleToFit zoom: scaleToFit
}); });
svgGroup.attr("transform", "translate(" + marginLeft + "," + (windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter) + ")scale(" + scaleToFit + ")"); svgGroup.attr("transform", "translate(0," + (windowHeight/2 - (nodeH*scaleToFit/2)) + ")scale(" + scaleToFit + ")");
zoomObj.translate([marginLeft - scaleToFit*marginLeft, windowHeight/2 - (nodeH*scaleToFit/2) + startNodeOffsetFromGraphCenter - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]); zoomObj.translate([0, windowHeight/2 - (nodeH*scaleToFit/2) - ((windowHeight/2 - rootH/2 - startNodeOffsetY)*scaleToFit)]);
} }
function update() { function update() {
if(scope.dimensionsSet) { 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") let links = svgGroup.selectAll(".WorkflowChart-link")
.data(scope.graphState.arrayOfLinksForChart, function(d) { return `${d.source.id}-${d.target.id}`; }); .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;}); .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id;});
baseSvg.selectAll(".WorkflowChart-linkPath") baseSvg.selectAll(".WorkflowChart-linkPath")
.transition()
.attr("class", function(d) { .attr("class", function(d) {
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath"; 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) { .attr('stroke', function(d) {
let edgeType = d.edgeType; let edgeType = d.edgeType;
if(edgeType) { if(edgeType) {
@@ -254,15 +312,64 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
linkClasses.push("WorkflowChart-link--active"); linkClasses.push("WorkflowChart-link--active");
} }
return linkClasses.join(' '); 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") baseSvg.selectAll(".WorkflowChart-circleBetweenNodes")
.attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-add";}) .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") 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 // Add any new links
let linkEnter = links.enter().append("g") 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";}) .attr("id", function(d){return "link-" + d.source.id + "-" + d.target.id + "-overlay";})
.call(edit_link) .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) { .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') { 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`); $(`#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); .classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass; let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) { if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
arrowClass = 'WorkflowChart-tooltipArrow--down'; arrowClass = 'WorkflowChart-tooltipArrow--down';
} else { } else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
arrowClass = 'WorkflowChart-tooltipArrow--right'; 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. // Add entering links in the parents old position.
linkEnter.append("line") linkEnter.insert("path", "g")
.attr("class", function(d) { .attr("class", function(d) {
return (d.source.id === scope.graphState.nodeBeingAdded || d.target.id === scope.graphState.nodeBeingAdded) ? "WorkflowChart-linkPath WorkflowChart-isNodeBeingAdded" : "WorkflowChart-linkPath"; 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) .call(edit_link)
.on("mouseenter", function(d) { .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') { 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); .classed("WorkflowChart-linkHovering", true);
let xPos, yPos, arrowClass; let xPos, yPos, arrowClass;
if (d.source.x === d.target.x) { if (nodePositionMap[d.source.id].y === nodePositionMap[d.target.id].y) {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - (100/2); xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 + 45;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 100; yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 107;
arrowClass = 'WorkflowChart-tooltipArrow--down'; arrowClass = 'WorkflowChart-tooltipArrow--down';
} else { } else {
xPos = d.source.y + nodeW + ((d.target.y - (d.source.y + nodeW))/2) - 115; xPos = (nodePositionMap[d.source.id].x + nodePositionMap[d.target.id].x)/2 - 30;
yPos = (d.source.x + nodeH/2 - d.target.x + nodeH/2)/2 + (d.target.x + nodeH/2) - 50; yPos = (nodePositionMap[d.source.id].y + nodePositionMap[d.target.id].y)/2 - 70;
arrowClass = 'WorkflowChart-tooltipArrow--right'; arrowClass = 'WorkflowChart-tooltipArrow--right';
} }
@@ -416,6 +540,23 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
.attr("r", 10) .attr("r", 10)
.attr("class", "WorkflowChart-addCircle WorkflowChart-circleBetweenNodes") .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; }) .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) .call(add_node_with_child)
.on("mouseover", function(d) { .on("mouseover", function(d) {
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); $(`#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") .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; }) .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) .call(add_node_with_child)
.on("mouseover", function(d) { .on("mouseover", function(d) {
$(`#link-${d.source.id}-${d.target.id}`).appendTo(`#aw-workflow-chart-g`); $(`#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); .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') let nodes = svgGroup.selectAll('.WorkflowChart-node')
.data(scope.graphState.arrayOfNodesForChart, function(d) { return d.id; }); .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(); nodes.exit().remove();
// Update existing nodes // 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") baseSvg.selectAll(".WorkflowChart-nodeAddCircle")
.style("display", function(d) { return scope.graphState.isLinkMode || d.id === scope.graphState.nodeBeingAdded || scope.readOnly ? "none" : null; }); .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() .enter()
.append('g') .append('g')
.attr("class", "WorkflowChart-node") .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) { nodeEnter.each(function(d) {
let thisNode = d3.select(this); 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.length > 1 && !graphLoaded) {
// if(scope.graphState.arrayOfNodesForChart && scope.graphState.arrayOfNodesForChart > 1 && !graphLoaded) { zoomToFitChart();
// zoomToFitChart(); }
// }
graphLoaded = true; graphLoaded = true;
// This will make sure that all the link elements appear before the nodes in the dom // 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(); 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){ else if(!scope.watchDimensionsSet){
scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){ scope.watchDimensionsSet = scope.$watch('dimensionsSet', function(){
@@ -1178,7 +1280,7 @@ export default ['$state','moment', '$timeout', '$window', '$filter', 'Rest', 'Ge
function node_click() { function node_click() {
this.on("click", function(d) { this.on("click", function(d) {
if(d.id !== scope.graphState.nodeBeingAdded && !scope.readOnly){ 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(); $('.WorkflowChart-potentialLink').remove();
scope.selectNodeForLinking({ scope.selectNodeForLinking({
nodeToStartLink: d nodeToStartLink: d

View File

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

View File

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

View File

@@ -3195,6 +3195,15 @@
"resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz",
"integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" "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": { "dashdash": {
"version": "1.14.1", "version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@@ -6185,6 +6194,14 @@
"integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
"dev": true "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": { "growl": {
"version": "1.9.2", "version": "1.9.2",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz",

View File

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