diff --git a/graph/layout.go b/graph/layout.go index 5dd389541..456c40505 100644 --- a/graph/layout.go +++ b/graph/layout.go @@ -1,5 +1,5 @@ /* - * Copyright IBM Corporation 2022 + * Copyright IBM Corporation 2023 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ func GetNodesAndEdges(graph graphtypes.Graph) ([]graphtypes.Node, []graphtypes.E } node := graphtypes.Node{ Id: fmt.Sprintf("v-%d", vertex.Id), - Position: graphtypes.Position{X: 0, Y: vertex.Iteration}, // store the iteration in the Y coordinate + Position: graphtypes.Position{X: 0, Y: vertex.Iteration}, // store the iteration in the Y coordinate (sub iterations in X coordinate) Data: graphtypes.Data{Label: vertex.Name, PathMappings: pathMappings}, } if vertex.Id == 0 { @@ -62,6 +62,31 @@ func GetNodesAndEdges(graph graphtypes.Graph) ([]graphtypes.Node, []graphtypes.E return nodes, edges } +type graphIterAndSubIter struct { + Iter int + Sub int +} + +type graphSortIters struct { + Items []graphIterAndSubIter +} + +// Len is the number of elements in the collection. +func (x *graphSortIters) Len() int { + return len(x.Items) +} + +// Less reports whether the element with index i +// must sort before the element with index j. +func (x *graphSortIters) Less(i, j int) bool { + return x.Items[i].Iter < x.Items[j].Iter || (x.Items[i].Iter == x.Items[j].Iter && x.Items[i].Sub < x.Items[j].Sub) +} + +// Swap swaps the elements with indexes i and j. +func (x *graphSortIters) Swap(i, j int) { + x.Items[i], x.Items[j] = x.Items[j], x.Items[i] +} + // BfsUpdatePositions updates the positions of the nodes using a layered layout algorithm that utilizes Breadth First Search. func BfsUpdatePositions(nodes []graphtypes.Node, edges []graphtypes.EdgeT) { adjMat := map[string]map[string]struct{}{} @@ -72,39 +97,56 @@ func BfsUpdatePositions(nodes []graphtypes.Node, edges []graphtypes.EdgeT) { adjMat[edge.Source][edge.Target] = struct{}{} } - // the bread first search algorithm - queue := []string{"v-0"} + iterSubIterToY := map[graphIterAndSubIter]int{} + { + // depth first search to calculate sub-iterations + subIters := map[graphIterAndSubIter]struct{}{} + visited := map[string]struct{}{"v-0": {}} + dfsRecursive(nodes, subIters, visited, adjMat, "v-0", -1) + sortIters := graphSortIters{} + for subIter := range subIters { + sortIters.Items = append(sortIters.Items, subIter) + } + sort.Stable(&sortIters) + for i, x := range sortIters.Items { + iterSubIterToY[x] = i * 200 + } + } + visited := map[string]struct{}{"v-0": {}} - xsPerIteration := map[int]int{} - for len(queue) > 0 { - // pop a vertex from the queue - current := queue[0] - queue = queue[1:] - // add its neighbours to the queue for processing later - neighbours := adjMat[current] - sortedNotVisitedNeighbours := []string{} - for neighbour := range neighbours { - if _, ok := visited[neighbour]; !ok { - sortedNotVisitedNeighbours = append(sortedNotVisitedNeighbours, neighbour) - visited[neighbour] = struct{}{} + iterSubIterToX := map[graphIterAndSubIter]int{} + { + // the bread first search algorithm + queue := []string{"v-0"} + for len(queue) > 0 { + // pop a vertex from the queue + current := queue[0] + queue = queue[1:] + // add its neighbours to the queue for processing later + neighbours := adjMat[current] + sortedNotVisitedNeighbours := []string{} + for neighbour := range neighbours { + if _, ok := visited[neighbour]; !ok { + sortedNotVisitedNeighbours = append(sortedNotVisitedNeighbours, neighbour) + visited[neighbour] = struct{}{} + } } + sort.StringSlice(sortedNotVisitedNeighbours).Sort() + queue = append(queue, sortedNotVisitedNeighbours...) + // calculate the new position for the current node + idx := common.FindIndex(nodes, func(n graphtypes.Node) bool { return n.Id == current }) + if idx < 0 { + logrus.Errorf("failed to find a node for the vertex with id '%s'", current) + continue + } + node := nodes[idx] + ii := graphIterAndSubIter{Iter: node.Position.Y, Sub: node.Position.X} + newX := iterSubIterToX[ii] + iterSubIterToX[ii] = newX + 200 + node.Position.X = newX + node.Position.Y = iterSubIterToY[ii] + nodes[idx] = node } - sort.StringSlice(sortedNotVisitedNeighbours).Sort() - queue = append(queue, sortedNotVisitedNeighbours...) - // calculate the new position for the current node - idx := common.FindIndex(nodes, func(n graphtypes.Node) bool { return n.Id == current }) - if idx < 0 { - logrus.Errorf("failed to find a node for the vertex with id '%s'", current) - continue - } - node := nodes[idx] - iteration := node.Position.Y - newX := xsPerIteration[iteration] - xsPerIteration[iteration] = newX + 200 - newY := iteration * 200 - node.Position.X = newX - node.Position.Y = newY - nodes[idx] = node } // handle islands @@ -115,12 +157,46 @@ func BfsUpdatePositions(nodes []graphtypes.Node, edges []graphtypes.EdgeT) { logrus.Errorf("found an unvisited node: %+v", node) visited[node.Id] = struct{}{} // calculate the new position for the current node - iteration := node.Position.Y - newX := xsPerIteration[iteration] - xsPerIteration[iteration] = newX + 200 - newY := iteration * 200 + ii := graphIterAndSubIter{Iter: node.Position.Y, Sub: node.Position.X} + newX := iterSubIterToX[ii] + iterSubIterToX[ii] = newX + 200 node.Position.X = newX - node.Position.Y = newY + node.Position.Y = iterSubIterToY[ii] nodes[i] = node } } + +func dfsRecursive( + nodes []graphtypes.Node, + subIters map[graphIterAndSubIter]struct{}, + visited map[string]struct{}, + adjMat map[string]map[string]struct{}, + current string, + parentIdx int, +) { + visited[current] = struct{}{} + currentIdx := common.FindIndex(nodes, func(n graphtypes.Node) bool { return n.Id == current }) + if currentIdx < 0 { + logrus.Errorf("failed to find the vertex with id '%s' in the list of nodes", current) + return + } + // calculate sub-iteration + currentNode := nodes[currentIdx] + if parentIdx >= 0 { + parentNode := nodes[parentIdx] + parentIteration := parentNode.Position.Y + currentIteration := currentNode.Position.Y + if parentIteration == currentIteration { + // store the sub iteration in the X coordinate + currentNode.Position.X = parentNode.Position.X + 1 + nodes[currentIdx] = currentNode + } + } + subIters[graphIterAndSubIter{Iter: currentNode.Position.Y, Sub: currentNode.Position.X}] = struct{}{} + neighbours := adjMat[current] + for neighbour := range neighbours { + if _, ok := visited[neighbour]; !ok { + dfsRecursive(nodes, subIters, visited, adjMat, neighbour, currentIdx) + } + } +} diff --git a/types/graph/graph.go b/types/graph/graph.go index 71bf2b5bf..63eb74e3f 100644 --- a/types/graph/graph.go +++ b/types/graph/graph.go @@ -28,10 +28,11 @@ type Graph struct { // Vertex is a single transformer. type Vertex struct { - Id int `json:"id"` - Iteration int `json:"iteration"` - Name string `json:"name"` - Data map[string]interface{} `json:"data,omitempty"` + Id int `json:"id"` + Iteration int `json:"iteration"` + SubIteration int `json:"subiteration"` + Name string `json:"name"` + Data map[string]interface{} `json:"data,omitempty"` } // Edge is an artifact.