Source code for pypath.visual.igraph_drawing

#!/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/>}), see
L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current
network from Cytoscape and convert it to igraph format.
"""

import sys
from collections import defaultdict
try:
    from itertools import izip
except ImportError:
    izip = zip

from math import atan2, cos, pi, sin, tan
from warnings import warn

try:
    from igraph._igraph import convex_hull, VertexSeq
    try:
        from igraph.compat import property
    except ModuleNotFoundError:
        pass
    from igraph.configuration import Configuration
    from igraph.drawing.baseclasses import AbstractDrawer, AbstractCairoDrawer, \
        AbstractXMLRPCDrawer
    from igraph.drawing.colors import color_to_html_format, color_name_to_rgb
    from pypath.visual.igraph_drawing.edge import ArrowEdgeDrawer
    from igraph.drawing.text import TextAlignment, TextDrawer
    from igraph.drawing.metamagic import AttributeCollectorBase
    from igraph.drawing.shapes import PolygonDrawer
    from igraph.drawing.utils import Point
    from pypath.visual.igraph_drawing.vertex import DefaultVertexDrawer
    from igraph.layout import Layout
    from igraph.drawing.graph import AbstractCairoGraphDrawer
    try:
        import cairo
    except ModuleNotFoundError:
        # No cairo support is installed. Create a fake module
        # pylint: disable-msg=C0103
        from igraph.drawing.utils import FakeModule
        cairo = FakeModule("igraph module could not be imported")

except ModuleNotFoundError:
    sys.stdout.write('Module `igraph` is not available.'
                     '\nSome plotting functionalities won\'t be accessible.\n')
[docs] class AbstractCairoGraphDrawer: pass
DefaultVertexDrawer = ArrowEdgeDrawer = TextDrawer = AbstractCairoGraphDrawer __all__ = ["DefaultGraphDrawerFFsupport"] __license__ = "GPL" #####################################################################
[docs] class DefaultGraphDrawerFFsupport(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_factory self.edge_drawer_factory = edge_drawer_factory self.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" in kwds: # Edge order specified explicitly return kwds["edge_order"] if kwds.get("edge_order_by") is None: # No edge order specified return None # Order edges by the value of some attribute edge_order_by = kwds["edge_order_by"] reverse = False if isinstance(edge_order_by, tuple): edge_order_by, reverse = edge_order_by if isinstance(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)) return edge_order def _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" in kwds: # Vertex order specified explicitly return kwds["vertex_order"] if kwds.get("vertex_order_by") is None: # No vertex order specified return None # Order vertices by the value of some attribute vertex_order_by = kwds["vertex_order_by"] reverse = False if isinstance(vertex_order_by, tuple): vertex_order_by, reverse = vertex_order_by if isinstance(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)) return vertex_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 :) def draw(self, graph, palette, *args, **kwds): # Some abbreviations for sake of simplicity directed = graph.is_directed() context = self.context # Calculate/get the layout of the graph layout = self.ensure_layout(kwds.get("layout", None), graph) # Determine the size of the margin on each side margin = kwds.get("margin", 0) try: margin = list(margin) except TypeError: margin = [margin] while len(margin) < 4: margin.extend(margin) # Contract the drawing area by the margin and fit the layout bbox = 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) if autocurve or (autocurve is None and "edge_curved" not in kwds and "curved" not in graph.edge_attributes() and graph.ecount() < 10000): from igraph import autocurve default = kwds.get("edge_curved", 0) if default is True: default = 0.5 default = float(default) kwds["edge_curved"] = autocurve( graph, attribute=None, default=default) # Construct the vertex, edge and label drawers vertex_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_drawer vertex_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 edges vertex_order = self._determine_vertex_order(graph, kwds) edge_order = self._determine_edge_order(graph, kwds) # Draw the highlighted groups (if any) if "mark_groups" in kwds: 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 pairs if isinstance(mark_groups, dict): group_iter = mark_groups.iteritems() elif hasattr(mark_groups, "__iter__"): # Lists, tuples, iterators etc group_iter = iter(mark_groups) else: # False group_iter = {}.iteritems() # We will need a polygon drawer to draw the convex hulls polygon_drawer = PolygonDrawer(context, bbox) # Iterate over color-memberlist pairs for group, color_id in group_iter: if not group or color_id is None: continue color = palette.get(color_id) if isinstance(group, VertexSeq): group = [vertex.index for vertex in group] if not hasattr(group, "__iter__"): raise TypeError("group membership list must be iterable") # Get the vertex indices that constitute the convex hull hull = [ group[i] for i in convex_hull([layout[idx] for idx in group]) ] # Calculate the preferred rounding radius for the corners corner_radius = 1.25 * \ max(vertex_builder[idx].size for idx in hull) # Construct the polygon polygon = [layout[idx] for idx in hull] if len(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 mass center = Point(*[ sum(coords) / float(len(coords)) for coords in zip(*polygon) ]) polygon = [ Point(*point).towards(center, -corner_radius) for point in polygon ] # Draw the hull context.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 edges es = graph.es if edge_order is None: # Default edge order edge_coord_iter = izip(es, edge_builder) else: # Specified edge order edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) # Draw the edges if directed: drawer_method = edge_drawer.draw_directed_edge else: drawer_method = edge_drawer.draw_undirected_edge for edge, visual_edge in edge_coord_iter: src, dest = edge.tuple src_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 vertices vs = graph.vs if vertex_order is None: # Default vertex order vertex_coord_iter = izip(vs, vertex_builder, layout) else: # Specified vertex order vertex_coord_iter = ((vs[i], vertex_builder[i], layout[i]) for i in vertex_order) # Draw the vertices drawer_method = vertex_drawer.draw context.set_line_width(1) for vertex, visual_vertex, coords in vertex_coord_iter: drawer_method(visual_vertex, vertex, coords) # Set the font we will use to draw the labels vertex_label_family = 'sans-serif' if not hasattr(graph, "vertex_label_family") \ else graph.vertex_label_family # Decide whether the labels have to be wrapped wrap = kwds.get("wrap_labels") if wrap is None: wrap = Configuration.instance()["plotting.wrap_labels"] wrap = bool(wrap) # Construct the iterator that we will use to draw the vertex labels if vertex_order is None: # Default vertex order vertex_coord_iter = izip(vertex_builder, layout) else: # Specified vertex order vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) # Draw the vertex labels for vertex, coords in vertex_coord_iter: if vertex.label is None: continue if hasattr(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.label if vertex.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.0 radius = 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. if w > 0: beta = atan2(h, w) % (2 * pi) else: beta = pi / 2. gamma = pi - beta if alpha > 2 * pi - beta or alpha <= beta: # Intersection at left edge of label cx += w cy -= tan(alpha) * w elif alpha > beta and alpha <= gamma: # Intersection at bottom edge of label try: cx += h / tan(alpha) except: pass # tan(alpha) == inf cy -= h elif alpha > gamma and alpha <= gamma + 2 * beta: # Intersection at right edge of label cx -= w cy += tan(alpha) * w else: # Intersection at top edge of label try: cx -= h / tan(alpha) except: pass # tan(alpha) == inf cy += h # Draw the label label_drawer.draw_at(cx - w, cy - h - yb, wrap=wrap) else: # Label is exactly in the center of the vertex cx, cy = coords half_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 labels es = graph.es if edge_order is None: # Default edge order edge_coord_iter = izip(es, edge_builder) else: # Specified edge order edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) # Draw the edge labels for edge, visual_edge in edge_coord_iter: if visual_edge.label is None: continue # Set the font size, color and text if hasattr(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 label src, dest = edge.tuple src_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.0 h /= 2.0 # Place the text relative to the edge if halign == TextAlignment.RIGHT: x -= w elif halign == TextAlignment.LEFT: x += w if valign == TextAlignment.BOTTOM: y -= h - yb / 2.0 elif valign == TextAlignment.TOP: y += h # Draw the edge label label_drawer.halign = halign label_drawer.valign = valign label_drawer.bbox = (x - w, y - h, x + w, y + h) label_drawer.draw(wrap=wrap)
#####################################################################