From b4fa03b2845bdb2f84b332037d73f9f79e40c9c9 Mon Sep 17 00:00:00 2001 From: AccentuSoft Date: Wed, 20 Jul 2022 22:41:25 +0300 Subject: [PATCH] Improved graph drawing in general (includes Cycle graphs). --- Core/Interface/CentralPane.py | 80 +++++++++++++++++------------------ LinkScope.py | 7 +-- 2 files changed, 42 insertions(+), 45 deletions(-) diff --git a/Core/Interface/CentralPane.py b/Core/Interface/CentralPane.py index 25562cc..9d7387b 100644 --- a/Core/Interface/CentralPane.py +++ b/Core/Interface/CentralPane.py @@ -1460,69 +1460,69 @@ class CanvasScene(QtWidgets.QGraphicsScene): # https://gitlab.com/graphviz/graphviz/-/merge_requests/2236 # No triangulation library on windows yet, so sfdp can't be used there. - if graphAlgorithm is None or graphAlgorithm not in ('sfdp', 'neato', 'dot'): + if graphAlgorithm is None or graphAlgorithm not in ('sfdp', 'neato', 'dot', 'circular'): graphAlgorithm = self.parent().mainWindow.SETTINGS.value("Program/GraphLayout", 'dot') + + # No real 'links' to group nodes by default (links to internal nodes don't count). This means that the + # graph algorithms can create odd graphs where group nodes are concerned. + # To fix this, we will duplicate the scene graph, add links between the group nodes and the + # outside world, and use that clone for positions. + currGraphClone = nx.DiGraph(self.sceneGraph) + + nodesToDel = set() + for edgeParentUID, edgeChild in dict(currGraphClone.edges): + potentialGroupIDOne = currGraphClone.nodes[edgeParentUID].get('groupID', edgeParentUID) + potentialGroupIDTwo = currGraphClone.nodes[edgeChild].get('groupID', edgeChild) + if potentialGroupIDOne != edgeParentUID: + nodesToDel.add(edgeParentUID) + if potentialGroupIDTwo != edgeChild: + nodesToDel.add(edgeChild) + # Nothing happens if edge already exists. If not, edge is created. + # These edges will not be visible on the actual graph generated. + currGraphClone.add_edge(potentialGroupIDOne, potentialGroupIDTwo) + + # Only use nodes that are represented on the graph, else we can end up with nodes on the + # other side of the world. + currGraphClone.remove_nodes_from(nodesToDel) + + pdGraph = nx.nx_pydot.to_pydot(currGraphClone) + try: if graphAlgorithm == 'sfdp': - self.scenePos = nx.nx_pydot.graphviz_layout(self.sceneGraph, 'sfdp') + pdGraph.set_layout('sfdp') + pdGraphNX = nx.nx_pydot.from_pydot(pdGraph) + self.scenePos = nx.nx_pydot.pydot_layout(pdGraphNX) xFactor = (0.40 + min(len(self.nodesDict) / 100, 10)) yFactor = (0.40 + min(len(self.nodesDict) / 100, 10)) elif graphAlgorithm == 'neato': - self.scenePos = nx.nx_pydot.graphviz_layout(self.sceneGraph, 'neato') + pdGraph.set_layout('neato') + pdGraphNX = nx.nx_pydot.from_pydot(pdGraph) + self.scenePos = nx.nx_pydot.pydot_layout(pdGraphNX) xFactor = (3 + (0.30 + min(len(self.nodesDict) / 100, 10))) yFactor = (3 + (0.30 + min(len(self.nodesDict) / 100, 10))) + elif graphAlgorithm == 'circular': + pdGraph.set_layout('circo') + pdGraphNX = nx.nx_pydot.from_pydot(pdGraph) + self.scenePos = nx.nx_pydot.pydot_layout(pdGraphNX) + xFactor = (0.40 + (0.30 + min(len(self.nodesDict) / 100, 10))) + yFactor = (0.40 + (0.30 + min(len(self.nodesDict) / 100, 10))) else: # Default algorithm is 'dot', when nothing else is selected. - - # No real 'links' to group nodes by default (links to internal nodes don't count). This means that the - # 'dot' algorithm can create odd graphs where group nodes are concerned. - # To fix this, we will duplicate the scene graph, add links between the group nodes and the - # outside world, and use that clone for positions. - currGraphClone = nx.DiGraph(self.sceneGraph) - - for edgeParentUID, edgeChild in dict(currGraphClone.edges): - potentialGroupIDOne = currGraphClone.nodes[edgeParentUID].get('groupID', edgeParentUID) - potentialGroupIDTwo = currGraphClone.nodes[edgeChild].get('groupID', edgeChild) - # Nothing happens if edge already exists. If not, edge is created. - # These edges will not be visible on the actual graph generated. - currGraphClone.add_edge(potentialGroupIDOne, potentialGroupIDTwo) - - # Only use nodes that are represented on the graph, else we can end up with nodes on the - # other side of the world. - currGraphClone = nx.DiGraph(self.sceneGraph) - for node in dict(currGraphClone.nodes): - if currGraphClone.nodes[node].get('groupID') is not None: - currGraphClone.remove_node(node) - - pdGraph = nx.drawing.nx_pydot.to_pydot(currGraphClone) pdGraph.set_layout('dot') pdGraph.set_rankdir('BT') pdGraphNX = nx.nx_pydot.from_pydot(pdGraph) - self.scenePos = nx.drawing.nx_pydot.pydot_layout(pdGraphNX) + self.scenePos = nx.nx_pydot.pydot_layout(pdGraphNX) xFactor = 0.70 yFactor = 2.75 except Exception as exc: self.parent().mainWindow.MESSAGEHANDLER.error('Failed drawing graph with selected algorithm, falling back ' 'to using "dot" algorithm: ' + str(exc), popUp=False) # If something goes wrong (e.g. the selected algorithm isn't found), use the dot algorithm. - # See comments for 'dot' algorithm. - currGraphClone = nx.DiGraph(self.sceneGraph) - for edgeParentUID, edgeChild in dict(currGraphClone.edges): - potentialGroupIDOne = currGraphClone.nodes[edgeParentUID].get('groupID', edgeParentUID) - potentialGroupIDTwo = currGraphClone.nodes[edgeChild].get('groupID', edgeChild) - currGraphClone.add_edge(potentialGroupIDOne, potentialGroupIDTwo) - - currGraphClone = nx.DiGraph(self.sceneGraph) - for node in dict(currGraphClone.nodes): - if currGraphClone.nodes[node].get('groupID') is not None: - currGraphClone.remove_node(node) - - pdGraph = nx.drawing.nx_pydot.to_pydot(currGraphClone) pdGraph.set_layout('dot') pdGraph.set_rankdir('BT') pdGraphNX = nx.nx_pydot.from_pydot(pdGraph) - self.scenePos = nx.drawing.nx_pydot.pydot_layout(pdGraphNX) + self.scenePos = nx.nx_pydot.pydot_layout(pdGraphNX) xFactor = 0.70 yFactor = 2.75 diff --git a/LinkScope.py b/LinkScope.py index c370c48..4cc11c0 100644 --- a/LinkScope.py +++ b/LinkScope.py @@ -585,7 +585,7 @@ class MainWindow(QtWidgets.QMainWindow): entitiesAlreadyInGroups.add(groupEntity) newCanvas.groupItemsProgrammatic(newGroup, 'Group ' + str(count)) count += 1 - newCanvas.rearrangeGraph('neato') + newCanvas.rearrangeGraph('circular') for cycleThread in list(self.cycleExtractionThreads): if cycleThread.isFinished(): @@ -4547,17 +4547,14 @@ class ExtractCyclesThread(QtCore.QThread): groups = [] allElements = set() - skipGrouping = True for cycle in list(itertools.zip_longest(*reorderedCycles)): cycleSet = set() for cycleElement in cycle: if cycleElement is not None: cycleSet.add(cycleElement) allElements.add(cycleElement) - if len(cycleSet) > 1 and not skipGrouping: + if len(cycleSet) > 1: groups.append(cycleSet) - elif skipGrouping: - skipGrouping = False self.cyclesSignal.emit([allElements, groups], self.canvasName)