private positionNodes()

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);
        }
    }