def show(self): """Saves the plot to a temporary file and shows it.""" if not isinstance(self._surface, cairo.ImageSurface): sur = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height)) ctx = cairo.Context(sur) self.redraw(ctx) else: sur = self._surface ctx = self._ctx if self._is_dirty: self.redraw(ctx) with named_temporary_file(prefix="igraph", suffix=".png") as tmpfile: sur.write_to_png(tmpfile) config = Configuration.instance() imgviewer = config["apps.image_viewer"] if not imgviewer: # No image viewer was given and none was detected. This # should only happen on unknown platforms. plat = platform.system() raise NotImplementedError("showing plots is not implemented " + \ "on this platform: %s" % plat) else: os.system("%s %s" % (imgviewer, tmpfile)) if platform.system() == "Darwin" or self._windows_hacks: # On Mac OS X and Windows, launched applications are likely to # fork and give control back to Python immediately. # Chances are that the temporary image file gets removed # before the image viewer has a chance to open it, so # we wait here a little bit. Yes, this is quite hackish :( time.sleep(5)
def main(): """The main entry point for igraph when invoked from the command line shell""" config = Configuration.instance() if config.filename: print("Using configuration from %s" % config.filename, file=sys.stderr) else: print("No configuration file, using defaults", file=sys.stderr) if "shells" in config: parts = [part.strip() for part in config["shells"].split(",")] shell_classes = [] available_classes = dict( [ (k, v) for k, v in globals().items() if isinstance(v, type) and issubclass(v, Shell) ] ) for part in parts: cls = available_classes.get(part, None) if cls is None: print("Warning: unknown shell class `%s'" % part, file=sys.stderr) continue shell_classes.append(cls) else: shell_classes = [IPythonShell, ClassicPythonShell] import platform if platform.system() == "Windows": shell_classes.insert(0, IDLEShell) shell = None for shell_class in shell_classes: # pylint: disable-msg=W0703 # W0703: catch "Exception" try: shell = shell_class() break except Exception: # Try the next one if "Classic" in str(shell_class): raise pass if isinstance(shell, Shell): if config["verbose"]: if shell.supports_progress_bar(): set_progress_handler(shell.get_progress_handler()) if shell.supports_status_messages(): set_status_handler(shell.get_status_handler()) shell() else: print("No suitable Python shell was found.", file=sys.stderr) print("Check configuration variable `general.shells'.", file=sys.stderr)
def main(): """The main entry point for igraph when invoked from the command line shell""" config = Configuration.instance() if config.filename: print >> sys.stderr, "Using configuration from %s" % config.filename else: print >> sys.stderr, "No configuration file, using defaults" if config.has_key("shells"): parts = [part.strip() for part in config["shells"].split(",")] shell_classes = [] available_classes = dict([(k, v) for k, v in globals().iteritems() if isinstance(v, type) and issubclass(v, Shell)]) for part in parts: klass = available_classes.get(part, None) if klass is None: print >> sys.stderr, "Warning: unknown shell class `%s'" % part continue shell_classes.append(klass) else: shell_classes = [IPythonShell, ClassicPythonShell] import platform if platform.system() == "Windows": shell_classes.insert(0, IDLEShell) shell = None for shell_class in shell_classes: # pylint: disable-msg=W0703 # W0703: catch "Exception" try: shell = shell_class() break except StandardError: # Try the next one if "Classic" in str(shell_class): raise pass if isinstance(shell, Shell): if config["verbose"]: if shell.supports_progress_bar(): set_progress_handler(shell.get_progress_handler()) if shell.supports_status_messages(): set_status_handler(shell.get_status_handler()) shell() else: print >> sys.stderr, "No suitable Python shell was found." print >> sys.stderr, "Check configuration variable `general.shells'."
def _get_response(self, path, params={}, compressed=False): """Sends a request to Nexus at the given path with the given parameters and returns a file-like object for the response. `compressed` denotes whether we accept compressed responses.""" if self.url is None: url = Configuration.instance()["remote.nexus.url"] else: url = self.url url = "%s%s?%s" % (url, path, urlencode(params)) request = urllib2.Request(url) if compressed: request.add_header("Accept-Encoding", "gzip") if self.debug: print "[debug] Sending request: %s" % url return self._opener.open(request)
def _get_response(self, path, params={}, compressed=False): """Sends a request to Nexus at the given path with the given parameters and returns a file-like object for the response. `compressed` denotes whether we accept compressed responses.""" if self.url is None: url = Configuration.instance()["remote.nexus.url"] else: url = self.url url = "%s%s?%s" % (url, path, urlencode(params)) request = urllib.request.Request(url) if compressed: request.add_header("Accept-Encoding", "gzip") if self.debug: print("[debug] Sending request: %s" % url) return self._opener.open(request)
def __init__(self, target=None, bbox=None, palette=None, background=None): """Creates a new plot. @param target: the target surface to write to. It can be one of the following types: - C{None} -- an appropriate surface will be created and the object will be plotted there. - C{cairo.Surface} -- the given Cairo surface will be used. - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. @param bbox: the bounding box of the surface. It is interpreted differently with different surfaces: PDF and PS surfaces will treat it as points (1 point = 1/72 inch). Image surfaces will treat it as pixels. SVG surfaces will treat it as an abstract unit, but it will mostly be interpreted as pixels when viewing the SVG file in Firefox. @param palette: the palette primarily used on the plot if the added objects do not specify a private palette. Must be either an L{igraph.drawing.colors.Palette} object or a string referring to a valid key of C{igraph.drawing.colors.palettes} (see module L{igraph.drawing.colors}) or C{None}. In the latter case, the default palette given by the configuration key C{plotting.palette} is used. @param background: the background color. If C{None}, the background will be transparent. You can use any color specification here that is understood by L{igraph.drawing.colors.color_name_to_rgba}. """ self._filename = None self._surface_was_created = not isinstance(target, cairo.Surface) self._need_tmpfile = False # Several Windows-specific hacks will be used from now on, thanks # to Dale Hunscher for debugging and fixing all that stuff self._windows_hacks = "Windows" in platform.platform() if bbox is None: self.bbox = BoundingBox(600, 600) elif isinstance(bbox, tuple) or isinstance(bbox, list): self.bbox = BoundingBox(bbox) else: self.bbox = bbox if palette is None: config = Configuration.instance() palette = config["plotting.palette"] if not isinstance(palette, Palette): palette = palettes[palette] self._palette = palette if target is None: self._need_tmpfile = True self._surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) ) elif isinstance(target, cairo.Surface): self._surface = target else: self._filename = target _, ext = os.path.splitext(target) ext = ext.lower() if ext == ".pdf": self._surface = cairo.PDFSurface( target, self.bbox.width, self.bbox.height ) elif ext == ".ps" or ext == ".eps": self._surface = cairo.PSSurface( target, self.bbox.width, self.bbox.height ) elif ext == ".png": self._surface = cairo.ImageSurface( cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) ) elif ext == ".svg": self._surface = cairo.SVGSurface( target, self.bbox.width, self.bbox.height ) else: raise ValueError("image format not handled by Cairo: %s" % ext) self._ctx = cairo.Context(self._surface) self._objects = [] self._is_dirty = False self.background = background
def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): """Plots the given object to the given target. Positional and keyword arguments not explicitly mentioned here will be passed down to the C{__plot__} method of the object being plotted. Since you are most likely interested in the keyword arguments available for graph plots, see L{Graph.__plot__} as well. @param obj: the object to be plotted @param target: the target where the object should be plotted. It can be one of the following types: - C{matplotib.axes.Axes} -- a matplotlib/pyplot axes in which the graph will be plotted. Drawing is delegated to the chosen matplotlib backend, and you can use interactive backends and matplotlib functions to save to file as well. - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. The supported image formats are: PNG, PDF, SVG and PostScript. - C{cairo.Surface} -- the given Cairo surface will be used. This can refer to a PNG image, an arbitrary window, an SVG file, anything that Cairo can handle. - C{None} -- a temporary file will be created and the object will be plotted there. igraph will attempt to open an image viewer and show the temporary file. This feature is deprecated from python-igraph version 0.9.1 and will be removed in 0.10.0. @param bbox: the bounding box of the plot. It must be a tuple with either two or four integers, or a L{BoundingBox} object. If this is a tuple with two integers, it is interpreted as the width and height of the plot (in pixels for PNG images and on-screen plots, or in points for PDF, SVG and PostScript plots, where 72 pt = 1 inch = 2.54 cm). If this is a tuple with four integers, the first two denotes the X and Y coordinates of a corner and the latter two denoting the X and Y coordinates of the opposite corner. @keyword opacity: the opacity of the object being plotted. It can be used to overlap several plots of the same graph if you use the same layout for them -- for instance, you might plot a graph with opacity 0.5 and then plot its spanning tree over it with opacity 0.1. To achieve this, you'll need to modify the L{Plot} object returned with L{Plot.add}. @keyword palette: the palette primarily used on the plot if the added objects do not specify a private palette. Must be either an L{igraph.drawing.colors.Palette} object or a string referring to a valid key of C{igraph.drawing.colors.palettes} (see module L{igraph.drawing.colors}) or C{None}. In the latter case, the default palette given by the configuration key C{plotting.palette} is used. @keyword margin: the top, right, bottom, left margins as a 4-tuple. If it has less than 4 elements or is a single float, the elements will be re-used until the length is at least 4. The default margin is 20 on each side. @keyword inline: whether to try and show the plot object inline in the current IPython notebook. Passing C{None} here or omitting this keyword argument will look up the preferred behaviour from the C{shell.ipython.inlining.Plot} configuration key. Note that this keyword argument has an effect only if igraph is run inside IPython and C{target} is C{None}. @return: an appropriate L{Plot} object. @see: Graph.__plot__ """ _, plt = find_matplotlib() if hasattr(plt, "Axes") and isinstance(target, plt.Axes): result = MatplotlibGraphDrawer(ax=target) result.draw(obj, *args, **kwds) return if not isinstance(bbox, BoundingBox): bbox = BoundingBox(bbox) result = Plot(target, bbox, background=kwds.get("background", "white")) if "margin" in kwds: bbox = bbox.contract(kwds["margin"]) del kwds["margin"] else: bbox = bbox.contract(20) result.add(obj, bbox, *args, **kwds) if target is None and _is_running_in_ipython(): # Get the default value of the `inline` argument from the configuration if # needed inline = kwds.get("inline") if inline is None: config = Configuration.instance() inline = config["shell.ipython.inlining.Plot"] # If we requested an inline plot, just return the result and IPython will # call its _repr_svg_ method. If we requested a non-inline plot, show the # plot in a separate window and return nothing if inline: return result else: result.show() return # We are either not in IPython or the user specified an explicit plot target, # so just show or save the result if target is None: result.show() elif isinstance(target, str): result.save() # Also return the plot itself return result
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"] # Deferred import to avoid a cycle in the import graph from igraph.clustering import VertexClustering, VertexCover # 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): # Dictionary mapping vertex indices or tuples of vertex # indices to colors group_iter = iter(mark_groups.items()) elif isinstance(mark_groups, (VertexClustering, VertexCover)): # Vertex clustering group_iter = ((group, color) for color, group in enumerate(mark_groups)) elif hasattr(mark_groups, "__iter__"): # Lists, tuples, iterators etc group_iter = iter(mark_groups) else: # False group_iter = iter({}.items()) # 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 = zip(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 = zip(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) # 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 = zip(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 # Set the font family, size, color and text context.select_font_face( vertex.font, 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.0 # 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.0 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.0 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 = zip(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 family, size, color and text context.select_font_face( visual_edge.font, 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)
def _collect_attributes(self, attr_spec, config=None): """Collects graph visualization attributes from various sources. This method can be used to collect the attributes required for graph visualization from various sources. Attribute value sources are: - A specific value of a Python dict belonging to a given key. This dict is given by the argument M{self.kwds} at construction time, and the name of the key is determined by the argument specification given in M{attr_spec}. - A vertex or edge sequence of a graph, given in M{self.seq} - The global configuration, given in M{config} - A default value when all other sources fail to provide the value. This is also given in M{attr_spec}. @param attr_spec: an L{AttributeSpecification} object which contains the name of the attribute when it is coming from a list of Python keyword arguments, the name of the attribute when it is coming from the graph attributes directly, the default value of the attribute and an optional callable transformation to call on the values. This can be used to ensure that the attributes are of a given type. @param config: a L{Configuration} object to be used for determining the defaults if all else fails. If C{None}, the global igraph configuration will be used @return: the collected attributes """ kwds = self.kwds seq = self.seq n = len(seq) # Special case if the attribute name is "label" if attr_spec.name == "label": if attr_spec.alt_name in kwds and kwds[attr_spec.alt_name] is None: return [None] * n # If the attribute uses an external callable to derive the attribute # values, call it and store the results if attr_spec.func is not None: func = attr_spec.func result = [func(i) for i in xrange(n)] return result # Get the configuration object if config is None: config = Configuration.instance() # Fetch the defaults from the vertex/edge sequence try: attrs = seq[attr_spec.name] except KeyError: attrs = None # Override them from the keyword arguments (if any) result = kwds.get(attr_spec.alt_name, None) if attrs: if not result: result = attrs else: if isinstance(result, str): result = [result] * n try: len(result) except TypeError: result = [result] * n result = [result[idx] or attrs[idx] for idx in xrange(len(result))] # Special case for string overrides, strings are not treated # as sequences here if isinstance(result, str): result = [result] * n # If the result is still not a sequence, make it one try: len(result) except TypeError: result = [result] * n # If it is not a list, ensure that it is a list if not hasattr(result, "extend"): result = list(result) # Ensure that the length is n while len(result) < n: if len(result) <= n / 2: result.extend(result) else: result.extend(result[0 : (n - len(result))]) # By now, the length of the result vector should be n as requested # Get the configuration defaults try: default = config["plotting.%s" % attr_spec.alt_name] except NoOptionError: default = None if default is None: default = attr_spec.default # Fill the None values with the default values for idx in xrange(len(result)): if result[idx] is None: result[idx] = default # Finally, do the transformation if attr_spec.transform is not None: transform = attr_spec.transform result = [transform(x) for x in result] return result
def __init__(self, target=None, bbox=None, palette=None, background=None): """Creates a new plot. @param target: the target surface to write to. It can be one of the following types: - C{None} -- an appropriate surface will be created and the object will be plotted there. - C{cairo.Surface} -- the given Cairo surface will be used. - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. @param bbox: the bounding box of the surface. It is interpreted differently with different surfaces: PDF and PS surfaces will treat it as points (1 point = 1/72 inch). Image surfaces will treat it as pixels. SVG surfaces will treat it as an abstract unit, but it will mostly be interpreted as pixels when viewing the SVG file in Firefox. @param palette: the palette primarily used on the plot if the added objects do not specify a private palette. Must be either an L{igraph.drawing.colors.Palette} object or a string referring to a valid key of C{igraph.drawing.colors.palettes} (see module L{igraph.drawing.colors}) or C{None}. In the latter case, the default palette given by the configuration key C{plotting.palette} is used. @param background: the background color. If C{None}, the background will be transparent. You can use any color specification here that is understood by L{igraph.drawing.colors.color_name_to_rgba}. """ self._filename = None self._surface_was_created = not isinstance(target, cairo.Surface) self._need_tmpfile = False # Several Windows-specific hacks will be used from now on, thanks # to Dale Hunscher for debugging and fixing all that stuff self._windows_hacks = "Windows" in platform.platform() if bbox is None: self.bbox = BoundingBox(600, 600) elif isinstance(bbox, tuple) or isinstance(bbox, list): self.bbox = BoundingBox(bbox) else: self.bbox = bbox if palette is None: config = Configuration.instance() palette = config["plotting.palette"] if not isinstance(palette, Palette): palette = palettes[palette] self._palette = palette if target is None: self._need_tmpfile = True self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ int(self.bbox.width), int(self.bbox.height)) elif isinstance(target, cairo.Surface): self._surface = target else: self._filename = target _, ext = os.path.splitext(target) ext = ext.lower() if ext == ".pdf": self._surface = cairo.PDFSurface(target, self.bbox.width, \ self.bbox.height) elif ext == ".ps" or ext == ".eps": self._surface = cairo.PSSurface(target, self.bbox.width, \ self.bbox.height) elif ext == ".png": self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ int(self.bbox.width), int(self.bbox.height)) elif ext == ".svg": self._surface = cairo.SVGSurface(target, self.bbox.width, \ self.bbox.height) else: raise ValueError("image format not handled by Cairo: %s" % ext) self._ctx = cairo.Context(self._surface) self._objects = [] self._is_dirty = False self.background = background
def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): """Plots the given object to the given target. Positional and keyword arguments not explicitly mentioned here will be passed down to the C{__plot__} method of the object being plotted. Since you are most likely interested in the keyword arguments available for graph plots, see L{Graph.__plot__} as well. @param obj: the object to be plotted @param target: the target where the object should be plotted. It can be one of the following types: - C{None} -- an appropriate surface will be created and the object will be plotted there. - C{cairo.Surface} -- the given Cairo surface will be used. This can refer to a PNG image, an arbitrary window, an SVG file, anything that Cairo can handle. - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. The supported image formats are: PNG, PDF, SVG and PostScript. @param bbox: the bounding box of the plot. It must be a tuple with either two or four integers, or a L{BoundingBox} object. If this is a tuple with two integers, it is interpreted as the width and height of the plot (in pixels for PNG images and on-screen plots, or in points for PDF, SVG and PostScript plots, where 72 pt = 1 inch = 2.54 cm). If this is a tuple with four integers, the first two denotes the X and Y coordinates of a corner and the latter two denoting the X and Y coordinates of the opposite corner. @keyword opacity: the opacity of the object being plotted. It can be used to overlap several plots of the same graph if you use the same layout for them -- for instance, you might plot a graph with opacity 0.5 and then plot its spanning tree over it with opacity 0.1. To achieve this, you'll need to modify the L{Plot} object returned with L{Plot.add}. @keyword palette: the palette primarily used on the plot if the added objects do not specify a private palette. Must be either an L{igraph.drawing.colors.Palette} object or a string referring to a valid key of C{igraph.drawing.colors.palettes} (see module L{igraph.drawing.colors}) or C{None}. In the latter case, the default palette given by the configuration key C{plotting.palette} is used. @keyword margin: the top, right, bottom, left margins as a 4-tuple. If it has less than 4 elements or is a single float, the elements will be re-used until the length is at least 4. The default margin is 20 on each side. @keyword inline: whether to try and show the plot object inline in the current IPython notebook. Passing ``None`` here or omitting this keyword argument will look up the preferred behaviour from the C{shell.ipython.inlining.Plot} configuration key. Note that this keyword argument has an effect only if igraph is run inside IPython and C{target} is C{None}. @return: an appropriate L{Plot} object. @see: Graph.__plot__ """ if not isinstance(bbox, BoundingBox): bbox = BoundingBox(bbox) result = Plot(target, bbox, background="white") if "margin" in kwds: bbox = bbox.contract(kwds["margin"]) del kwds["margin"] else: bbox = bbox.contract(20) result.add(obj, bbox, *args, **kwds) if IN_IPYTHON and target is None: # Get the default value of the `inline` argument from the configuration if # needed inline = kwds.get("inline") if inline is None: config = Configuration.instance() inline = config["shell.ipython.inlining.Plot"] # If we requested an inline plot, just return the result and IPython will # call its _repr_svg_ method. If we requested a non-inline plot, show the # plot in a separate window and return nothing if inline: return result else: result.show() return # We are either not in IPython or the user specified an explicit plot target, # so just show or save the result if target is None: result.show() elif isinstance(target, basestring): result.save() # Also return the plot itself return result
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"] # Deferred import to avoid a cycle in the import graph from igraph.clustering import VertexClustering, VertexCover # 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): # Dictionary mapping vertex indices or tuples of vertex # indices to colors group_iter = mark_groups.iteritems() elif isinstance(mark_groups, (VertexClustering, VertexCover)): # Vertex clustering group_iter = ( (group, color) for color, group in enumerate(mark_groups) ) 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) # 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 # Set the font family, size, color and text context.select_font_face(vertex.font, \ 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 family, size, color and text context.select_font_face(visual_edge.font, \ 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)
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) # margin = [x + 20. for x in margin[:4]] # Contract the drawing area by the margin and fit the layout bbox = self.bbox.contract(margin) layout.fit_into(bbox, 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 visual vertex/edge builders class VisualVertexBuilder(AttributeCollectorBase): """Collects some visual properties of a vertex for drawing""" _kwds_prefix = "vertex_" color = ("green", palette.get) label = None label_angle = -pi/2 label_dist = 0.0 label_color = ("black", palette.get) label_size = 14.0 position = dict(func=layout.__getitem__) shape = ("circle", ShapeDrawerDirectory.resolve_default) size = 20.0 # RON EDIT --------------------- edge_color = ("#444", palette.get) edge_size = 2.0 # ------------------------------ class VisualEdgeBuilder(AttributeCollectorBase): """Collects some visual properties of an edge for drawing""" _kwds_prefix = "edge_" arrow_size = 1.0 arrow_width = 1.0 color = ("#444", palette.get) curved = (0.0, ArrowEdgeDrawer._curvature_to_float) width = 1.0 vertex_builder = VisualVertexBuilder(graph.vs, kwds) edge_builder = VisualEdgeBuilder(graph.es, 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() # Draw the edges edge_drawer = self.edge_drawer_factory(context) if directed: drawer_method = edge_drawer.draw_directed_edge else: drawer_method = edge_drawer.draw_undirected_edge for edge, visual_edge in izip(graph.es, edge_builder): src, dest = edge.tuple src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] drawer_method(visual_edge, src_vertex, dest_vertex) # Calculate the desired vertex order if "vertex_order" in kwds: # Vertex order specified explicitly vertex_order = kwds["vertex_order"] elif kwds.get("vertex_order_by") is not None: # Vertex order by another attribute vertex_order_by = kwds["vertex_order_by"] if isinstance(vertex_order_by, tuple): vertex_order_by, reverse = vertex_order_by if isinstance(reverse, basestring) and reverse.lower().startswith("asc"): reverse = False else: reverse = bool(reversed) else: reverse = False attrs = graph.vs[vertex_order_by] vertex_order = sorted(range(graph.vcount()), key=attrs.__getitem__, reverse=reverse) del attrs else: # Default vertex order vertex_order = None 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) # RON EDIT --------------------- # Draw the vertices context.set_line_width(1) for vertex, coords in vertex_coord_iter: vertex.shape.draw_path(context, coords[0], coords[1], vertex.size) context.set_source_rgba(*vertex.color) context.fill_preserve() #print vertex.edge_color context.set_source_rgba(*vertex.edge_color) context.set_line_width(vertex.edge_size) context.stroke() # ------------------------------ # Draw the vertex labels context.select_font_face("sans-serif", cairo.FONT_SLANT_NORMAL, \ cairo.FONT_WEIGHT_NORMAL) wrap = kwds.get("wrap_labels", None) if wrap is None: wrap = Configuration.instance()["plotting.wrap_labels"] else: wrap = bool(wrap) 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) label_drawer = self.label_drawer_factory(context) for vertex, coords in vertex_coord_iter: if vertex.label is None: continue 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)
def _collect_attributes(self, attr_spec, config=None): """Collects graph visualization attributes from various sources. This method can be used to collect the attributes required for graph visualization from various sources. Attribute value sources are: - A specific value of a Python dict belonging to a given key. This dict is given by the argument M{self.kwds} at construction time, and the name of the key is determined by the argument specification given in M{attr_spec}. - A vertex or edge sequence of a graph, given in M{self.seq} - The global configuration, given in M{config} - A default value when all other sources fail to provide the value. This is also given in M{attr_spec}. @param attr_spec: an L{AttributeSpecification} object which contains the name of the attribute when it is coming from a list of Python keyword arguments, the name of the attribute when it is coming from the graph attributes directly, the default value of the attribute and an optional callable transformation to call on the values. This can be used to ensure that the attributes are of a given type. @param config: a L{Configuration} object to be used for determining the defaults if all else fails. If C{None}, the global igraph configuration will be used @return: the collected attributes """ kwds = self.kwds seq = self.seq n = len(seq) # Special case if the attribute name is "label" if attr_spec.name == "label": if attr_spec.alt_name in kwds and kwds[attr_spec.alt_name] is None: return [None] * n # If the attribute uses an external callable to derive the attribute # values, call it and store the results if attr_spec.func is not None: func = attr_spec.func result = [func(i) for i in range(n)] return result # Get the configuration object if config is None: config = Configuration.instance() # Fetch the defaults from the vertex/edge sequence try: attrs = seq[attr_spec.name] except KeyError: attrs = None # Override them from the keyword arguments (if any) result = kwds.get(attr_spec.alt_name, None) if attrs: if not result: result = attrs else: if isinstance(result, str): result = [result] * n try: len(result) except TypeError: result = [result] * n result = [result[idx] or attrs[idx] \ for idx in range(len(result))] # Special case for string overrides, strings are not treated # as sequences here if isinstance(result, str): result = [result] * n # If the result is still not a sequence, make it one try: len(result) except TypeError: result = [result] * n # If it is not a list, ensure that it is a list if not hasattr(result, "extend"): result = list(result) # Ensure that the length is n while len(result) < n: if len(result) <= n / 2: result.extend(result) else: result.extend(result[0:(n - len(result))]) # By now, the length of the result vector should be n as requested # Get the configuration defaults try: default = config["plotting.%s" % attr_spec.alt_name] except NoOptionError: default = None if default is None: default = attr_spec.default # Fill the None values with the default values for idx in range(len(result)): if result[idx] is None: result[idx] = default # Finally, do the transformation if attr_spec.transform is not None: transform = attr_spec.transform result = [transform(x) for x in result] return result
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) # margin = [x + 20. for x in margin[:4]] # Contract the drawing area by the margin and fit the layout bbox = self.bbox.contract(margin) layout.fit_into(bbox, 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 visual vertex/edge builders class VisualVertexBuilder(AttributeCollectorBase): """Collects some visual properties of a vertex for drawing""" _kwds_prefix = "vertex_" color = ("red", palette.get) frame_color = ("black", palette.get) frame_width = 1.0 label = None label_angle = -pi / 2 label_dist = 0.0 label_color = ("black", palette.get) label_size = 14.0 position = dict(func=layout.__getitem__) shape = ("circle", ShapeDrawerDirectory.resolve_default) size = 20.0 class VisualEdgeBuilder(AttributeCollectorBase): """Collects some visual properties of an edge for drawing""" _kwds_prefix = "edge_" arrow_size = 1.0 arrow_width = 1.0 color = ("#444", palette.get) curved = (0.0, ArrowEdgeDrawer._curvature_to_float) width = 1.0 vertex_builder = VisualVertexBuilder(graph.vs, kwds) edge_builder = VisualEdgeBuilder(graph.es, 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() # Draw the edges edge_drawer = self.edge_drawer_factory(context) if directed: drawer_method = edge_drawer.draw_directed_edge else: drawer_method = edge_drawer.draw_undirected_edge for edge, visual_edge in izip(graph.es, edge_builder): src, dest = edge.tuple src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] drawer_method(visual_edge, src_vertex, dest_vertex) # Calculate the desired vertex order if "vertex_order" in kwds: # Vertex order specified explicitly vertex_order = kwds["vertex_order"] elif kwds.get("vertex_order_by") is not None: # Vertex order by another attribute vertex_order_by = kwds["vertex_order_by"] if isinstance(vertex_order_by, tuple): vertex_order_by, reverse = vertex_order_by if isinstance( reverse, basestring) and reverse.lower().startswith("asc"): reverse = False else: reverse = bool(reversed) else: reverse = False attrs = graph.vs[vertex_order_by] vertex_order = sorted(range(graph.vcount()), key=attrs.__getitem__, reverse=reverse) del attrs else: # Default vertex order vertex_order = None 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 vertices context.set_line_width(1) for vertex, coords in vertex_coord_iter: vertex.shape.draw_path(context, \ coords[0], coords[1], vertex.size) context.set_source_rgba(*vertex.color) context.fill_preserve() context.set_source_rgba(*vertex.frame_color) context.set_line_width(vertex.frame_width) context.stroke() # Draw the vertex labels context.select_font_face("sans-serif", cairo.FONT_SLANT_NORMAL, \ cairo.FONT_WEIGHT_NORMAL) wrap = kwds.get("wrap_labels", None) if wrap is None: wrap = Configuration.instance()["plotting.wrap_labels"] else: wrap = bool(wrap) 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) label_drawer = self.label_drawer_factory(context) for vertex, coords in vertex_coord_iter: if vertex.label is None: continue 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)