Source code for pyworkflow.gui.graph_layout

# **************************************************************************
# *
# * Authors:     J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
# *
# * Unidad de  Bioinformatica of Centro Nacional de Biotecnologia , CSIC
# *
# * This program is free software; you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation; either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program; if not, write to the Free Software
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
# * 02111-1307  USA
# *
# *  All comments concerning this program package may be sent to the
# *  e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
import logging

logger = logging.getLogger(__name__)

from pyworkflow import Config, SCIPION_DEFAULT_FONT_SIZE



[docs]class GraphLayout(object): """ Base class for all algorithm that implement functions to organize a graph in a plane. """ def __init__(self): super().__init__() self.DY = 65 self.DX = 15 self._fontScaleFactor = None
[docs] def getY(self): """ :return: Y distance affected by the font size """ return self.DY*self.getFontScaleFactor()
[docs] def getFontScaleFactor(self): """ :return: The scale factor between default font size 10, and current one """ if self._fontScaleFactor is None: self._fontScaleFactor = Config.SCIPION_FONT_SIZE/SCIPION_DEFAULT_FONT_SIZE return self._fontScaleFactor
[docs] def draw(self, graph, **kwargs): """ Setup the nodes position in the plane. """ pass
[docs]class LevelTreeLayout(GraphLayout): """ Organize the nodes of the graph by levels. It will recursively organize children and then fit the sibling trees. """ def __init__(self, partial=False): GraphLayout.__init__(self) self.maxLevel = 9999 self.partial = partial
[docs] def draw(self, graph, **kwargs): """ Organize nodes of the graph in the plane. Nodes should have x, y, width and height attributes. x and y will be modified. """ rootNode = graph.getRoot() # Setup empty layout for each node for node in graph.getNodes(): node._layout = {} visitDict = dict() # Do some level initialization on each node self._setLayoutLevel(rootNode, 1, visitDict) self._computeNodeOffsets(rootNode, 1) # Compute extreme left limit m = 9999 for left, _ in rootNode._layout['hLimits']: m = min(m, left) self._applyNodeOffsets(rootNode, -m + self.getY()) # Clean temporary _layout attributes for node in graph.getNodes(): del node._layout
def _isNewNode(self, node): return node.x == 0 or node.y == 0 or node.isRoot() def _setLayoutLevel(self, node, level, parent, ancestors=[]): """ Iterate over all nodes and set _layout dict. Also set the node level, which is defined as the max level of a parent + 1 """ if level > self.maxLevel: return layout = node._layout if level > layout.get('level', 0): # Calculate the y-position depending on the level # and the delta-Y (DY) if not self.partial or self._isNewNode(node): node.y = level * self.getY() layout['level'] = level layout['parent'] = parent if hasattr(node, 'width'): half = node.width / 2 else: half = 50 layout['half'] = half layout['hLimits'] = [[-half, half]] layout['offset'] = 0 if self.__isNodeExpanded(node): ancestors.append(node.getName()) for child in node.getChildren(): if child.getName() in ancestors: logger.warning("WARNING: There might be a cyclic redundancy error in this protocol: %s (%s)" %(child.getLabel(), child.getName())) if Config.debugOn(): print("%s: Setting layout for child %s" % ("-" * level, child), flush=True) self._setLayoutLevel(child, level+1, node, ancestors.copy()) def __isNodeExpanded(self, node): """ Check if the status of the node is expanded or collapsed. """ return getattr(node, 'expanded', True) def __setNodeOffset(self, node, offset): node._layout['offset'] = offset def __getNodeHalf(self, node): return node._layout['half'] def __getNodeChilds(self, node): """ Return the node's childs that have been visited by this node first (its 'parent') """ if self.__isNodeExpanded(node): return [c for c in node.getChildren() if c._layout['parent'] is node] else: return [] # treat collapsed nodes as if they have no childs def _computeNodeOffsets(self, node, level): """ Position a parent node and its childs. Only this sub-tree will be considered at this point. Then it will be adjusted with node siblings. """ if level > self.maxLevel: return childs = self.__getNodeChilds(node) n = len(childs) if n > 0: for c in childs: self._computeNodeOffsets(c, level + 1) if n > 1: offset = 0 # Keep right limits to compute the separation between siblings # some times it not enough to compare with the left sibling # for some child levels of the node rightLimits = [r for l, r in childs[0]._layout['hLimits']] for i in range(n-1): sep = self._getChildsSeparation(childs[i], childs[i+1], rightLimits) offset += sep c = childs[i+1] self.__setNodeOffset(c, offset) half0 = self.__getNodeHalf(childs[0]) total = half0 + offset + self.__getNodeHalf(childs[-1]) half = total / 2 for c in childs: self.__setNodeOffset(c, c._layout['offset'] - half + half0) else: self.__setNodeOffset(childs[0], 0) self._computeHLimits(node) def _computeHLimits(self, node): """ This function will traverse the tree from node to build the left and right profiles(hLimits) for each level of the tree """ layout = node._layout hLimits = layout['hLimits'] childs = self.__getNodeChilds(node) for child in childs: count = 1 layout = child._layout for l, r in layout['hLimits']: l += layout['offset'] r += layout['offset'] if count < len(hLimits): if l < hLimits[count][0]: hLimits[count][0] = l if r > hLimits[count][1]: hLimits[count][1] = r else: hLimits.append([l, r]) count += 1 def _getChildsSeparation(self, child1, child2, rightLimits): """Calculate separation between siblings at each height level""" sep = 0 hL2 = child2._layout['hLimits'] n1 = len(rightLimits) n2 = len(hL2) h = min(n1, n2) for i in range(h): right = rightLimits[i] left = hL2[i][0] if left + sep < right: sep = right - left rightLimits[i] = hL2[i][1] if n1 > n2: # If there are more levels in the rightLimits # updated the last ones like if they belong # to next sibling is is now (sep + self.DX) away for i in range(h, n1): rightLimits[i] -= sep + self.DX else: # If the current right sibling has more levels # just add them to the current rightLimits for i in range(h, n2): rightLimits.append(hL2[i][1]) return sep + self.DX def _applyNodeOffsets(self, node, x): """ Adjust the x-position of the nodes by applying the offsets. """ if node._layout['level'] == self.maxLevel: return layout = node._layout if not self.partial or self._isNewNode(node): node.x = x + layout['offset'] childs = self.__getNodeChilds(node) for child in childs: self._applyNodeOffsets(child, node.x)