#!/usr/bin/env python# -*- coding: utf-8 -*-## This file is part of the `pypath` python module## Copyright 2014-2023# EMBL, EMBL-EBI, Uniklinik RWTH Aachen, Heidelberg University## Authors: see the file `README.rst`# Contact: Dénes Türei (turei.denes@gmail.com)## Distributed under the GPLv3 License.# See accompanying file LICENSE.txt or copy at# https://www.gnu.org/licenses/gpl-3.0.html## Website: https://pypath.omnipathdb.org/#"""Drawing routines to draw graphs.This module contains routines to draw graphs on: - Cairo surfaces (L{DefaultGraphDrawer}) - UbiGraph displays (L{UbiGraphDrawer}, see U{http://ubietylab.net/ubigraph})It also contains routines to send an igraph graph directly to(U{Cytoscape<http://www.cytoscape.org>}) using the(U{CytoscapeRPC plugin<http://gforge.nbic.nl/projects/cytoscaperpc/>}), seeL{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the currentnetwork from Cytoscape and convert it to igraph format."""importsysfromcollectionsimportdefaultdicttry:fromitertoolsimportizipexceptImportError:izip=zipfrommathimportatan2,cos,pi,sin,tanfromwarningsimportwarntry:fromigraph._igraphimportconvex_hull,VertexSeqtry:fromigraph.compatimportpropertyexceptModuleNotFoundError:passfromigraph.configurationimportConfigurationfromigraph.drawing.baseclassesimportAbstractDrawer,AbstractCairoDrawer, \
AbstractXMLRPCDrawerfromigraph.drawing.colorsimportcolor_to_html_format,color_name_to_rgbfrompypath.visual.igraph_drawing.edgeimportArrowEdgeDrawerfromigraph.drawing.textimportTextAlignment,TextDrawerfromigraph.drawing.metamagicimportAttributeCollectorBasefromigraph.drawing.shapesimportPolygonDrawerfromigraph.drawing.utilsimportPointfrompypath.visual.igraph_drawing.verteximportDefaultVertexDrawerfromigraph.layoutimportLayoutfromigraph.drawing.graphimportAbstractCairoGraphDrawertry:importcairoexceptModuleNotFoundError:# No cairo support is installed. Create a fake module# pylint: disable-msg=C0103fromigraph.drawing.utilsimportFakeModulecairo=FakeModule("igraph module could not be imported")exceptModuleNotFoundError:sys.stdout.write('Module `igraph` is not available.''\nSome plotting functionalities won\'t be accessible.\n')
[docs]classDefaultGraphDrawerFFsupport(AbstractCairoGraphDrawer):"""Class implementing the default visualisation of a graph. The default visualisation of a graph draws the nodes on a 2D plane according to a given L{Layout}, then draws a straight or curved edge between nodes connected by edges. This is the visualisation used when one invokes the L{plot()} function on a L{Graph} object. See L{Graph.__plot__()} for the keyword arguments understood by this drawer."""
[docs]def__init__(self,context,bbox,vertex_drawer_factory=DefaultVertexDrawer,edge_drawer_factory=ArrowEdgeDrawer,label_drawer_factory=TextDrawer):"""Constructs the graph drawer and associates it to the given Cairo context and the given L{BoundingBox}. @param context: the context on which we will draw @param bbox: the bounding box within which we will draw. Can be anything accepted by the constructor of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple or a L{BoundingBox} object). @param vertex_drawer_factory: a factory method that returns an L{AbstractCairoVertexDrawer} instance bound to a given Cairo context. The factory method must take three parameters: the Cairo context, the bounding box of the drawing area and the palette to be used for drawing colored vertices. The default vertex drawer is L{DefaultVertexDrawer}. @param edge_drawer_factory: a factory method that returns an L{AbstractEdgeDrawer} instance bound to a given Cairo context. The factory method must take two parameters: the Cairo context and the palette to be used for drawing colored edges. You can use any of the actual L{AbstractEdgeDrawer} implementations here to control the style of edges drawn by igraph. The default edge drawer is L{ArrowEdgeDrawer}. @param label_drawer_factory: a factory method that returns a L{TextDrawer} instance bound to a given Cairo context. The method must take one parameter: the Cairo context. The default label drawer is L{TextDrawer}. """AbstractCairoGraphDrawer.__init__(self,context,bbox)self.vertex_drawer_factory=vertex_drawer_factoryself.edge_drawer_factory=edge_drawer_factoryself.label_drawer_factory=label_drawer_factory
def_determine_edge_order(self,graph,kwds):"""Returns the order in which the edge of the given graph have to be drawn, assuming that the relevant keyword arguments (C{edge_order} and C{edge_order_by}) are given in C{kwds} as a dictionary. If neither C{edge_order} nor C{edge_order_by} is present in C{kwds}, this function returns C{None} to indicate that the graph drawer is free to choose the most convenient edge ordering."""if"edge_order"inkwds:# Edge order specified explicitlyreturnkwds["edge_order"]ifkwds.get("edge_order_by")isNone:# No edge order specifiedreturnNone# Order edges by the value of some attributeedge_order_by=kwds["edge_order_by"]reverse=Falseifisinstance(edge_order_by,tuple):edge_order_by,reverse=edge_order_byifisinstance(reverse,basestring):reverse=reverse.lower().startswith("desc")attrs=graph.es[edge_order_by]edge_order=sorted(range(len(attrs)),key=attrs.__getitem__,reverse=bool(reverse))returnedge_orderdef_determine_vertex_order(self,graph,kwds):"""Returns the order in which the vertices of the given graph have to be drawn, assuming that the relevant keyword arguments (C{vertex_order} and C{vertex_order_by}) are given in C{kwds} as a dictionary. If neither C{vertex_order} nor C{vertex_order_by} is present in C{kwds}, this function returns C{None} to indicate that the graph drawer is free to choose the most convenient vertex ordering."""if"vertex_order"inkwds:# Vertex order specified explicitlyreturnkwds["vertex_order"]ifkwds.get("vertex_order_by")isNone:# No vertex order specifiedreturnNone# Order vertices by the value of some attributevertex_order_by=kwds["vertex_order_by"]reverse=Falseifisinstance(vertex_order_by,tuple):vertex_order_by,reverse=vertex_order_byifisinstance(reverse,basestring):reverse=reverse.lower().startswith("desc")attrs=graph.vs[vertex_order_by]vertex_order=sorted(range(len(attrs)),key=attrs.__getitem__,reverse=bool(reverse))returnvertex_order# pylint: disable-msg=W0142,W0221,E1101# W0142: Used * or ** magic# W0221: argument number differs from overridden method# E1101: Module 'cairo' has no 'foo' member - of course it does :)defdraw(self,graph,palette,*args,**kwds):# Some abbreviations for sake of simplicitydirected=graph.is_directed()context=self.context# Calculate/get the layout of the graphlayout=self.ensure_layout(kwds.get("layout",None),graph)# Determine the size of the margin on each sidemargin=kwds.get("margin",0)try:margin=list(margin)exceptTypeError:margin=[margin]whilelen(margin)<4:margin.extend(margin)# Contract the drawing area by the margin and fit the layoutbbox=self.bbox.contract(margin)layout.fit_into(bbox,keep_aspect_ratio=kwds.get("keep_aspect_ratio",False))# Decide whether we need to calculate the curvature of edges# automatically -- and calculate them if needed.autocurve=kwds.get("autocurve",None)ifautocurveor(autocurveisNoneand"edge_curved"notinkwdsand"curved"notingraph.edge_attributes()andgraph.ecount()<10000):fromigraphimportautocurvedefault=kwds.get("edge_curved",0)ifdefaultisTrue:default=0.5default=float(default)kwds["edge_curved"]=autocurve(graph,attribute=None,default=default)# Construct the vertex, edge and label drawersvertex_drawer=self.vertex_drawer_factory(context,bbox,palette,layout)edge_drawer=self.edge_drawer_factory(context,palette)label_drawer=self.label_drawer_factory(context)# Construct the visual vertex/edge builders based on the specifications# provided by the vertex_drawer and the edge_drawervertex_builder=vertex_drawer.VisualVertexBuilder(graph.vs,kwds)edge_builder=edge_drawer.VisualEdgeBuilder(graph.es,kwds)# Determine the order in which we will draw the vertices and edgesvertex_order=self._determine_vertex_order(graph,kwds)edge_order=self._determine_edge_order(graph,kwds)# Draw the highlighted groups (if any)if"mark_groups"inkwds:mark_groups=kwds["mark_groups"]# Figure out what to do with mark_groups in order to be able to# iterate over it and get memberlist-color pairsifisinstance(mark_groups,dict):group_iter=mark_groups.iteritems()elifhasattr(mark_groups,"__iter__"):# Lists, tuples, iterators etcgroup_iter=iter(mark_groups)else:# Falsegroup_iter={}.iteritems()# We will need a polygon drawer to draw the convex hullspolygon_drawer=PolygonDrawer(context,bbox)# Iterate over color-memberlist pairsforgroup,color_idingroup_iter:ifnotgrouporcolor_idisNone:continuecolor=palette.get(color_id)ifisinstance(group,VertexSeq):group=[vertex.indexforvertexingroup]ifnothasattr(group,"__iter__"):raiseTypeError("group membership list must be iterable")# Get the vertex indices that constitute the convex hullhull=[group[i]foriinconvex_hull([layout[idx]foridxingroup])]# Calculate the preferred rounding radius for the cornerscorner_radius=1.25* \
max(vertex_builder[idx].sizeforidxinhull)# Construct the polygonpolygon=[layout[idx]foridxinhull]iflen(polygon)==2:# Expand the polygon (which is a flat line otherwise)a,b=Point(*polygon[0]),Point(*polygon[1])c=corner_radius*(a-b).normalized()n=Point(-c[1],c[0])polygon=[a+n,b+n,b-c,b-n,a-n,a+c]else:# Expand the polygon around its center of masscenter=Point(*[sum(coords)/float(len(coords))forcoordsinzip(*polygon)])polygon=[Point(*point).towards(center,-corner_radius)forpointinpolygon]# Draw the hullcontext.set_source_rgba(color[0],color[1],color[2],color[3]*0.25)polygon_drawer.draw_path(polygon,corner_radius=corner_radius)context.fill_preserve()context.set_source_rgba(*color)context.stroke()# Construct the iterator that we will use to draw the edgeses=graph.esifedge_orderisNone:# Default edge orderedge_coord_iter=izip(es,edge_builder)else:# Specified edge orderedge_coord_iter=((es[i],edge_builder[i])foriinedge_order)# Draw the edgesifdirected:drawer_method=edge_drawer.draw_directed_edgeelse:drawer_method=edge_drawer.draw_undirected_edgeforedge,visual_edgeinedge_coord_iter:src,dest=edge.tuplesrc_vertex,dest_vertex=vertex_builder[src],vertex_builder[dest]drawer_method(visual_edge,src_vertex,dest_vertex)# Construct the iterator that we will use to draw the verticesvs=graph.vsifvertex_orderisNone:# Default vertex ordervertex_coord_iter=izip(vs,vertex_builder,layout)else:# Specified vertex ordervertex_coord_iter=((vs[i],vertex_builder[i],layout[i])foriinvertex_order)# Draw the verticesdrawer_method=vertex_drawer.drawcontext.set_line_width(1)forvertex,visual_vertex,coordsinvertex_coord_iter:drawer_method(visual_vertex,vertex,coords)# Set the font we will use to draw the labelsvertex_label_family='sans-serif'ifnothasattr(graph,"vertex_label_family") \
elsegraph.vertex_label_family# Decide whether the labels have to be wrappedwrap=kwds.get("wrap_labels")ifwrapisNone:wrap=Configuration.instance()["plotting.wrap_labels"]wrap=bool(wrap)# Construct the iterator that we will use to draw the vertex labelsifvertex_orderisNone:# Default vertex ordervertex_coord_iter=izip(vertex_builder,layout)else:# Specified vertex ordervertex_coord_iter=((vertex_builder[i],layout[i])foriinvertex_order)# Draw the vertex labelsforvertex,coordsinvertex_coord_iter:ifvertex.labelisNone:continueifhasattr(vertex,'label_family'):context.select_font_face(vertex.label_family,cairo.FONT_SLANT_NORMAL,cairo.FONT_WEIGHT_NORMAL)context.set_font_size(vertex.label_size)context.set_source_rgba(*vertex.label_color)label_drawer.text=vertex.labelifvertex.label_dist:# Label is displaced from the center of the vertex._,yb,w,h,_,_=label_drawer.text_extents()w,h=w/2.0,h/2.0radius=vertex.label_dist*vertex.size/2.# First we find the reference point that is at distance `radius'# from the vertex in the direction given by `label_angle'.# Then we place the label in a way that the line connecting the# center of the bounding box of the label with the center of the# vertex goes through the reference point and the reference# point lies exactly on the bounding box of the vertex.alpha=vertex.label_angle%(2*pi)cx=coords[0]+radius*cos(alpha)cy=coords[1]-radius*sin(alpha)# Now we have the reference point. We have to decide which side# of the label box will intersect with the line that connects# the center of the label with the center of the vertex.ifw>0:beta=atan2(h,w)%(2*pi)else:beta=pi/2.gamma=pi-betaifalpha>2*pi-betaoralpha<=beta:# Intersection at left edge of labelcx+=wcy-=tan(alpha)*welifalpha>betaandalpha<=gamma:# Intersection at bottom edge of labeltry:cx+=h/tan(alpha)except:pass# tan(alpha) == infcy-=helifalpha>gammaandalpha<=gamma+2*beta:# Intersection at right edge of labelcx-=wcy+=tan(alpha)*welse:# Intersection at top edge of labeltry:cx-=h/tan(alpha)except:pass# tan(alpha) == infcy+=h# Draw the labellabel_drawer.draw_at(cx-w,cy-h-yb,wrap=wrap)else:# Label is exactly in the center of the vertexcx,cy=coordshalf_size=vertex.size/2.label_drawer.bbox=(cx-half_size,cy-half_size,cx+half_size,cy+half_size)label_drawer.draw(wrap=wrap)# Construct the iterator that we will use to draw the edge labelses=graph.esifedge_orderisNone:# Default edge orderedge_coord_iter=izip(es,edge_builder)else:# Specified edge orderedge_coord_iter=((es[i],edge_builder[i])foriinedge_order)# Draw the edge labelsforedge,visual_edgeinedge_coord_iter:ifvisual_edge.labelisNone:continue# Set the font size, color and textifhasattr(visual_edge,'label_family'):context.select_font_face(visual_edge.label_family,cairo.FONT_SLANT_NORMAL,cairo.FONT_WEIGHT_NORMAL)context.set_font_size(visual_edge.label_size)context.set_source_rgba(*visual_edge.label_color)label_drawer.text=visual_edge.label# Ask the edge drawer to propose an anchor point for the labelsrc,dest=edge.tuplesrc_vertex,dest_vertex=vertex_builder[src],vertex_builder[dest](x,y),(halign,valign)= \
edge_drawer.get_label_position(edge,src_vertex,dest_vertex)# Measure the text_,yb,w,h,_,_=label_drawer.text_extents()w/=2.0h/=2.0# Place the text relative to the edgeifhalign==TextAlignment.RIGHT:x-=welifhalign==TextAlignment.LEFT:x+=wifvalign==TextAlignment.BOTTOM:y-=h-yb/2.0elifvalign==TextAlignment.TOP:y+=h# Draw the edge labellabel_drawer.halign=halignlabel_drawer.valign=valignlabel_drawer.bbox=(x-w,y-h,x+w,y+h)label_drawer.draw(wrap=wrap)