in nifi-frontend/src/main/frontend/apps/nifi/src/app/pages/provenance/ui/provenance-event-listing/provenance-event-table/lineage/lineage.component.ts [371:587]
private positionNodes(nodeIds: string[], depth: number, parents: string[], levelDifference: number): void {
const { width } = this.lineageElement.getBoundingClientRect();
const immediateSet: Set<string> = new Set(nodeIds);
const childSet: Set<string> = new Set();
const descendantSet: Set<string> = new Set();
// locate children
this.locateDescendants(nodeIds, childSet, 1);
// locate all descendants (including children)
this.locateDescendants(nodeIds, descendantSet);
// push off processing a node until its deepest point
// by removing any descendants from the immediate nodes.
// in this case, a link is panning multiple levels
descendantSet.forEach(function (d) {
immediateSet.delete(d);
});
// convert the children to an array to ensure consistent
// order when performing index of checks below
const children: string[] = Array.from(childSet.values()).sort(d3.descending);
// convert the immediate to allow for sorting below
let immediate: string[] = Array.from(immediateSet.values());
// attempt to identify fan in/out cases
let nodesWithTwoParents = 0;
immediate.forEach((nodeId) => {
const node: any = this.nodeLookup.get(nodeId);
// identify fanning cases
if (node.incoming.length > 3) {
levelDifference = LineageComponent.DEFAULT_LEVEL_DIFFERENCE;
} else if (node.incoming.length >= 2) {
nodesWithTwoParents++;
}
});
// increase the level difference if more than two nodes have two or more parents
if (nodesWithTwoParents > 2) {
levelDifference = LineageComponent.DEFAULT_LEVEL_DIFFERENCE;
}
// attempt to sort the nodes to provide an optimum layout
if (parents.length === 1) {
immediate = immediate.sort((one: string, two: string) => {
const oneNode: any = this.nodeLookup.get(one);
const twoNode: any = this.nodeLookup.get(two);
// try to order by children
if (oneNode.outgoing.length > 0 && twoNode.outgoing.length > 0) {
const oneIndex: number = children.indexOf(oneNode.outgoing[0].target.id);
const twoIndex: number = children.indexOf(twoNode.outgoing[0].target.id);
if (oneIndex !== twoIndex) {
return oneIndex - twoIndex;
}
}
// try to order by parents
if (oneNode.incoming.length > 0 && twoNode.incoming.length > 0) {
const oneIndex: number = oneNode.incoming[0].source.index;
const twoIndex: number = twoNode.incoming[0].source.index;
if (oneIndex !== twoIndex) {
return oneIndex - twoIndex;
}
}
// type of node
if (oneNode.type !== twoNode.type) {
return oneNode.type > twoNode.type ? 1 : -1;
}
// type of event
if (oneNode.eventType !== twoNode.eventType) {
return oneNode.eventType > twoNode.eventType ? 1 : -1;
}
// timestamp
return oneNode.millis - twoNode.millis;
});
} else if (parents.length > 1) {
immediate = immediate.sort((one: string, two: string) => {
const oneNode: any = this.nodeLookup.get(one);
const twoNode: any = this.nodeLookup.get(two);
// try to order by parents
if (oneNode.incoming.length > 0 && twoNode.incoming.length > 0) {
const oneIndex: number = oneNode.incoming[0].source.index;
const twoIndex: number = twoNode.incoming[0].source.index;
if (oneIndex !== twoIndex) {
return oneIndex - twoIndex;
}
}
// try to order by children
if (oneNode.outgoing.length > 0 && twoNode.outgoing.length > 0) {
const oneIndex: number = children.indexOf(oneNode.outgoing[0].target.id);
const twoIndex: number = children.indexOf(twoNode.outgoing[0].target.id);
if (oneIndex !== twoIndex) {
return oneIndex - twoIndex;
}
}
// node type
if (oneNode.type !== twoNode.type) {
return oneNode.type > twoNode.type ? 1 : -1;
}
// event type
if (oneNode.eventType !== twoNode.eventType) {
return oneNode.eventType > twoNode.eventType ? 1 : -1;
}
// timestamp
return oneNode.millis - twoNode.millis;
});
}
let originX: number = width / 2;
if (parents.length > 0) {
const meanParentX: number | undefined = d3.mean(parents, (parentId: string) => {
const parent = this.nodeLookup.get(parentId);
return parent ? parent.x : undefined;
});
if (meanParentX) {
originX = meanParentX;
}
}
const depthWidth: number = (immediate.length - 1) * LineageComponent.DEFAULT_NODE_SPACING;
immediate.forEach((nodeId: string, i: number) => {
const node: any = this.nodeLookup.get(nodeId);
// set the y position based on the depth
node.y = levelDifference + depth - 25;
// ensure the children won't position on top of one another
// based on the number of parent nodes
if (immediate.length <= parents.length) {
if (node.incoming.length === 1) {
const parent: any = node.incoming[0].source;
if (parent.outgoing.length === 1) {
node.x = parent.x;
return;
}
} else if (node.incoming.length > 1) {
const nodesOnPreviousLevel: any = node.incoming.filter((link: any) => {
return node.y - link.source.y <= LineageComponent.DEFAULT_LEVEL_DIFFERENCE;
});
node.x = d3.mean(nodesOnPreviousLevel, function (link: any) {
return link.source.x;
});
return;
}
}
// evenly space the nodes under the origin
node.x = i * LineageComponent.DEFAULT_NODE_SPACING + originX - depthWidth / 2;
});
// sort the immediate nodes after positioning by the x coordinate
// so they can be shifted accordingly if necessary
const sortedImmediate: string[] = immediate.slice().sort((one: string, two: string) => {
const nodeOne: any = this.nodeLookup.get(one);
const nodeTwo: any = this.nodeLookup.get(two);
return nodeOne.x - nodeTwo.x;
});
// adjust the x positioning if necessary to avoid positioning on top
// of one another, only need to consider the x coordinate since the
// y coordinate will be the same for each node on this row
for (let i = 0; i < sortedImmediate.length - 1; i++) {
const first: any = this.nodeLookup.get(sortedImmediate[i]);
const second: any = this.nodeLookup.get(sortedImmediate[i + 1]);
const difference: number = second.x - first.x;
if (difference < LineageComponent.DEFAULT_NODE_SPACING) {
second.x += LineageComponent.DEFAULT_NODE_SPACING - difference;
}
}
// if there are children to position
if (children.length > 0) {
let childLevelDifference: number = LineageComponent.DEFAULT_LEVEL_DIFFERENCE / 3;
// resort the immediate values after each node has been positioned
immediate = immediate.sort((one, two) => {
const oneNode: any = this.nodeLookup.get(one);
const twoNode: any = this.nodeLookup.get(two);
return oneNode.x - twoNode.x;
});
// mark each nodes index so subsequent recursive calls can position children accordingly
let nodesWithTwoChildren = 0;
immediate.forEach((nodeId: string, i: number) => {
const node: any = this.nodeLookup.get(nodeId);
node.index = i;
// precompute the next level difference since we have easy access to going here
if (node.outgoing.length > 3) {
childLevelDifference = LineageComponent.DEFAULT_LEVEL_DIFFERENCE;
} else if (node.outgoing.length >= 2) {
nodesWithTwoChildren++;
}
});
// if there are at least two immediate nodes with two or more children, increase the level difference
if (nodesWithTwoChildren > 2) {
childLevelDifference = LineageComponent.DEFAULT_LEVEL_DIFFERENCE;
}
// position the children
this.positionNodes(children, levelDifference + depth, immediate, childLevelDifference);
}
}