in ui-modules/app-inspector/app/components/workflow/workflow-steps.directive.js [80:309]
function makeArrows(workflow, steps, options) {
const sectionWidth = ((options || {}).width) || 56;
workflow = workflow || {};
workflow.data = workflow.data || {};
let [stepsPrev,stepsNext] = getWorkflowStepsPrevNext(workflow);
const arrows = [];
const strokeWidth = 1.5;
const arrowheadLength = 6;
const arrowheadWidth = arrowheadLength/3/strokeWidth;
const defs = [];
defs.push('<marker id="arrowhead" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon fill="#000" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><');
defs.push('<marker id="arrowhead-gray" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon class="fill-future" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><');
defs.push('<marker id="arrowhead-red" markerWidth="'+3*arrowheadWidth+'" markerHeight="'+3*arrowheadWidth+'" refX="'+0+'" refY="'+1.5*arrowheadWidth+'" orient="auto"><polygon class="fill-failed" points="0 0, '+3*arrowheadWidth+' '+1.5*arrowheadWidth+', 0 '+(3*arrowheadWidth)+'" /></marker><');
if (steps) {
let gradientCount = 0;
function arrowSvg(y1, y2, opts) {
var start = y1==='start/end';
var end = y2==='start/end';
if (y1==null || y2==null || (start&&end)) {
// ignore if out of bounds
return "";
}
if (!opts) opts = {};
const color = opts.class ? '' : opts.color || (opts.colorEnd && opts.colorEnd==opts.colorStart ? opts.colorEnd : '#000');
const rightFarEdge = sectionWidth;
const rightArrowheadStart = rightFarEdge - arrowheadLength;
const leftFarEdge = 10;
const leftActive = rightArrowheadStart + (leftFarEdge - rightArrowheadStart) * (opts.width || 1);
const curveX = opts.curveX || 1;
const curveY = opts.curveY || 1;
// const controlPointRightFarEdge = rightFarEdge + (leftActive - rightFarEdge) * curveX;
const controlPointRightArrowheadStart = rightArrowheadStart + (leftActive - rightArrowheadStart) * curveX;
// average of above two, to see which works best
// const controlPointRightIntermediate = (rightFarEdge+rightArrowheadStart)/2 + (leftActive - (rightFarEdge+rightArrowheadStart)/2) * curveX;
// const controlPointRightExaggerated = rightArrowheadStart + (leftActive - rightFarEdge) * curveX;
const controlPointStart = controlPointRightArrowheadStart;
const controlPointEnd = controlPointRightArrowheadStart;
const strokeConstant = color ? 'stroke="'+color+'"' : ''
let standard =
'stroke-width="'+(opts.lineWidth || strokeWidth)+'" '+
'fill="transparent" '+
'/>';
if (opts.class) standard = 'class="'+opts.class+'" '+standard;
if (!opts.hideArrowhead) standard = 'marker-end="url(#'+(opts.arrowheadId || 'arrowhead')+')" ' +standard;
if (opts.dashLength) standard = 'stroke-dasharray="'+opts.dashLength+'" '+standard;
if (start) {
return '<path d="M ' + leftFarEdge + ' ' + y2 +
' L ' + rightArrowheadStart + ' ' + y2 + '" '+
strokeConstant+' '+standard;
}
if (end) {
return '<path d="M ' + rightFarEdge + ' ' + y1 +
' L ' + (leftFarEdge+arrowheadLength) + ' ' + y1 + '" '+
strokeConstant+' '+standard;
}
const yMCH = ((y2 - y1) / 2) * curveY;
const yM = (y1 + y2) / 2;
if (!opts.colorEnd || opts.colorEnd==opts.colorStart || y2==y1) {
standard = strokeConstant + ' ' + standard;
} else {
const gradientId = 'gradient'+(gradientCount++);
const gradY = y2>=y1 ? 'y2="1"' : 'y1="1"';
defs.push('<linearGradient id="'+gradientId+'" x2="0" '+gradY+'><stop offset="0" stop-color="'+opts.colorStart+'"/><stop offset="1" stop-color="'+opts.colorEnd+'"/></linearGradient>');
standard = 'stroke="url(#'+gradientId+')" ' + standard;
}
const result = '<path d="M ' + rightFarEdge + ' ' + y1 +
// ' L ' + r0 + ' ' + y1 + ' ' +
' C ' + controlPointStart + ' ' + y1 + ', ' + leftActive + ' ' + (yM - yMCH) + ', ' + leftActive + ' ' + yM + ' ' +
' S ' + controlPointEnd + ' ' + y2 + ', ' + rightArrowheadStart + ' ' + y2 + '" '+standard;
return result;
}
function stepY(n) {
if (n==-1) return 'start/end';
if (!steps || n<0 || n>=steps.length || _.isNil(n)) {
console.log("workflow arrow bounds error", steps, n);
return null;
}
return steps[n].offsetTop + steps[n].offsetHeight / 2;
}
function arrowStep(n1, n2, opts) {
let s1 = stepY(n1);
let s2 = stepY(n2);
const deltaForArrowMax = 6;
const deltaForArrowTarget = 0.125;
if (typeof s1 === "number") s1 += Math.min(steps[n1].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
if (typeof s2 === "number") s2 -= Math.min(steps[n2].offsetHeight * deltaForArrowTarget, deltaForArrowMax);
return arrowSvg(s1, s2, opts);
}
let jumpSizes = {1: true};
function arrowStep2(prev, i, opts) {
let curveX = 0.5;
let curveY = 0.75;
let width = 0.5;
if (prev==-1 || i==-1) {
// curve values don't matter for start/end
} else if (prev==i) {
width = 0.15;
curveX = 0.1;
curveY = 0.75;
} else {
let rank = jumpSizes.indexOf(''+Math.abs(prev-i));
if (rank<0) {
console.log("Missing workflow link: ", prev, i);
rank = 0;
}
if (prev > i) rank = rank + 0.5;
width = 0.2 + 0.6 * (rank + 0.5) / (jumpSizes.length + 0.5);
curveX = 0.8 + 0.2*width;
curveY = 0.8 + 0.2*width;
// higher values (above) look nicer, but make disambiguation of complex paths harder
// curveX = 0.5 + 0.3*width;
// curveY = 0.4 + 0.4*width;
}
return arrowStep(prev, i, {hideArrowhead: prev==i, width, curveX, curveY, ...opts});
}
function colorFor(step, references) {
if (!references) return 'red';
const i = references.indexOf(step);
if (i==-3) return 'red';
// skew quadratically for lightness
const skewTowards1 = x => (1 - (1-x)*(1-x));
let gray = Math.round(240 * skewTowards1(i / references.length) );
return 'rgb('+gray+','+gray+','+gray+')';
}
let arrowSpecs = {};
function recordTransition(from, to, opts) {
if (to!=-1 && from!=-1 && to!=from) {
jumpSizes[Math.abs(from-to)] = true;
}
if (to<0) to=-1; // in record, -2 means end, -3 means error; here -1 means end because nothing should go to -1
if (arrowSpecs[[from,to]]) {
// prefer earlier additions (real steps) over theoretical ones
} else {
arrowSpecs[[from, to]] = {from, to, ...(opts || {})};
}
}
for (var i = -3; i < steps.length; i++) {
const prevsHere = stepsPrev[i];
if (prevsHere && prevsHere.length) {
prevsHere.forEach(prev => {
// last in list has higher z-order; this ensures within each prevStep we preserve order,
// so inbound arrows are correct. currently we also prefer earlier steps, which isn't quite right for outbound arrows;
// ideally we'd reconstruct the flow order, but that's a bit more work than we want to do just now.
// so insertion point is always 0. (header items added at end so we don't need to include those here.)
recordTransition(prev, i, { insertionPoint: 0, visited: true, colorStart: colorFor(i, stepsNext[prev]), colorEnd: colorFor(prev, prevsHere) });
});
}
}
// now make pale arrows for the default flow
var indexOfId = {};
for (var i = 0; i < steps.length; i++) {
const s = workflow.data.stepsDefinition[i];
if (!s) console.log("Missing step", i, workflow.data, steps);
if (s.id) indexOfId[s.id] = i;
}
function isStepType(step, type) {
if (!step) return false;
if (step.type) return step.type == type;
let s = step.startsWith ? step : step.s || step.shorthand || step.userSuppliedShorthand;
if (s) return s == type || s.startsWith(type);
return false;
}
for (var i = 0; i < steps.length; i++) {
const s = workflow.data.stepsDefinition[i];
let opts = { insertionPoint: 0 };
// errors shown elsewhere
// if (workflow.data.currentStepIndex === i && workflow.data.status && workflow.data.status.startsWith('ERROR')) {
// recordTransition(i, -2, { ...opts, class: 'arrow-failed', arrowheadId: 'arrowhead-red' });
// }
opts = { ...opts, class: 'arrow-future', arrowheadId: 'arrowhead-gray', dashLength: 8 };
let next = null;
if (s.next) {
if (s.next.toLowerCase()=='end') next = -1;
else if (indexOfId[s.next]) next = indexOfId[s.next];
}
if (isStepType(s, 'return')) next = -1;
if (next!=null) {
// special next per step
recordTransition(i, next, opts);
if (!s.condition) continue;
}
// if nothing special, or if was conditional, then go to next step
// (only go forward 1, even if it is conditional, otherwise too many arrows)
next = i+1;
if (i + 1 >= steps.length) next = -1;
recordTransition(i, next, opts);
}
jumpSizes = Object.keys(jumpSizes).sort();
// insert arrows
Object.values(arrowSpecs).forEach(arrowSpec =>
arrows.splice(arrowSpec.insertionPoint || 0, 0, arrowStep2(arrowSpec.from, arrowSpec.to, arrowSpec)) );
// then defs at start
arrows.splice(0, 0, '<defs>'+defs.join('')+'</defs>');
}
return arrows;
}