def draw(self, graph, *args, **kwds): # NOTE: matplotlib has numpy as a dependency, so we can use it in here import matplotlib as mpl import matplotlib.markers as mmarkers from matplotlib.path import Path from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle import numpy as np def shrink_vertex(ax, aux, vcoord, vsize_squared): """Shrink edge by vertex size""" aux_display, vcoord_display = ax.transData.transform([aux, vcoord]) d = sqrt(((aux_display - vcoord_display) ** 2).sum()) fr = sqrt(vsize_squared) / d end_display = vcoord_display + fr * (aux_display - vcoord_display) end = ax.transData.inverted().transform(end_display) return end def callback_factory(ax, vcoord, vsizes, arrows): def callback_edge_offset(event): for arrow, src, tgt in arrows: v1, v2 = vcoord[src], vcoord[tgt] # This covers both cases (curved and straight) aux1, aux2 = arrow._path_original.vertices[[1, -2]] start = shrink_vertex(ax, aux1, v1, vsizes[src]) end = shrink_vertex(ax, aux2, v2, vsizes[tgt]) arrow._path_original.vertices[0] = start arrow._path_original.vertices[-1] = end return callback_edge_offset ax = self.ax # FIXME: deal with unnamed *args # Get layout layout = kwds.get("layout", graph.layout()) if isinstance(layout, str): layout = graph.layout(layout) # Vertex coordinates vcoord = layout.coords # Vertex properties nv = graph.vcount() # Vertex size vsizes = kwds.get("vertex_size", 5) # Enforce numpy array for sizes, because (1) we need the square and (2) # they are needed to calculate autoshrinking of edges if np.isscalar(vsizes): vsizes = np.repeat(vsizes, nv) else: vsizes = np.asarray(vsizes) # ax.scatter uses the *square* of diameter vsizes **= 2 # Vertex color c = kwds.get("vertex_color", "steelblue") # Vertex opacity alpha = kwds.get("alpha", 1.0) # Vertex labels label = kwds.get("vertex_label", None) # Vertex label size label_size = kwds.get("vertex_label_size", mpl.rcParams["font.size"]) # Vertex zorder vzorder = kwds.get("vertex_order", 2) # Vertex shapes # mpl shapes use slightly different names from Cairo, but we want the # API to feel consistent, so we use a conversion dictionary shapes = kwds.get("vertex_shape", "o") if shapes is not None: if isinstance(shapes, str): shapes = self._shape_dict.get(shapes, shapes) elif isinstance(shapes, mmarkers.MarkerStyle): pass # Scatter vertices x, y = list(zip(*vcoord)) ax.scatter(x, y, s=vsizes, c=c, marker=shapes, zorder=vzorder, alpha=alpha) # Vertex labels if label is not None: for i, lab in enumerate(label): xi, yi = x[i], y[i] ax.text(xi, yi, lab, fontsize=label_size) dx = max(x) - min(x) dy = max(y) - min(y) ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) # Edge properties ne = graph.ecount() ec = kwds.get("edge_color", "black") edge_width = kwds.get("edge_width", 1) arrow_width = kwds.get("edge_arrow_width", 2) arrow_length = kwds.get("edge_arrow_size", 4) ealpha = kwds.get("edge_alpha", 1.0) ezorder = kwds.get("edge_order", 1.0) try: ezorder = float(ezorder) ezorder = [ezorder] * ne except TypeError: pass # 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) # Arrow style for directed and undirected graphs if graph.is_directed(): arrowstyle = ArrowStyle( "-|>", head_length=arrow_length, head_width=arrow_width, ) else: arrowstyle = "-" # Edge coordinates and curvature nloops = [0 for x in range(ne)] has_curved = "curved" in graph.es.attributes() arrows = [] for ie, edge in enumerate(graph.es): src, tgt = edge.source, edge.target x1, y1 = vcoord[src] x2, y2 = vcoord[tgt] # Loops require special treatment if src == tgt: # Find all non-loop edges nloopstot = 0 angles = [] for tgtn in graph.neighbors(src): if tgtn == src: nloopstot += 1 continue xn, yn = vcoord[tgtn] angles.append(180.0 / pi * atan2(yn - y1, xn - x1) % 360) # with .neighbors(mode=ALL), which is default, loops are double # counted nloopstot //= 2 angles = sorted(set(angles)) # Only loops or one non-loop if len(angles) < 2: ashift = angles[0] if angles else 270 if nloopstot == 1: # Only one self loop, use a quadrant only angles = [(ashift + 135) % 360, (ashift + 225) % 360] else: nshift = 360.0 / nloopstot angles = [ (ashift + nshift * nloops[src]) % 360, (ashift + nshift * (nloops[src] + 1)) % 360, ] nloops[src] += 1 else: angles.append(angles[0] + 360) idiff = 0 diff = 0 for i in range(len(angles) - 1): diffi = abs(angles[i + 1] - angles[i]) if diffi > diff: idiff = i diff = diffi angles = angles[idiff : idiff + 2] ashift = angles[0] nshift = (angles[1] - angles[0]) / nloopstot angles = [ (ashift + nshift * nloops[src]), (ashift + nshift * (nloops[src] + 1)), ] nloops[src] += 1 # this is not great, but alright angspan = angles[1] - angles[0] if angspan < 180: angmid1 = angles[0] + 0.1 * angspan angmid2 = angles[1] - 0.1 * angspan else: angmid1 = angles[0] + 0.5 * (angspan - 180) + 45 angmid2 = angles[1] - 0.5 * (angspan - 180) - 45 aux1 = ( x1 + 0.2 * dx * cos(pi / 180 * angmid1), y1 + 0.2 * dy * sin(pi / 180 * angmid1), ) aux2 = ( x1 + 0.2 * dx * cos(pi / 180 * angmid2), y1 + 0.2 * dy * sin(pi / 180 * angmid2), ) start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) path = Path( [start, aux1, aux2, end], # Cubic bezier by mpl codes=[1, 4, 4, 4], ) else: curved = edge["curved"] if has_curved else False if curved: aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( 2 * y1 + y2 ) / 3.0 + edge.curved * 0.5 * (x2 - x1) aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( y1 + 2 * y2 ) / 3.0 + edge.curved * 0.5 * (x2 - x1) start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) path = Path( [start, aux1, aux2, end], # Cubic bezier by mpl codes=[1, 4, 4, 4], ) else: start = shrink_vertex(ax, (x2, y2), (x1, y1), vsizes[src]) end = shrink_vertex(ax, (x1, y1), (x2, y2), vsizes[tgt]) path = Path([start, end], codes=[1, 2]) arrow = FancyArrowPatch( path=path, arrowstyle=arrowstyle, lw=edge_width, color=ec, alpha=ealpha, zorder=ezorder[ie], ) ax.add_artist(arrow) # Store arrows and their sources and targets for autoscaling arrows.append((arrow, src, tgt)) # Autoscaling during zoom, figure resize, reset axis limits callback = callback_factory(ax, vcoord, vsizes, arrows) ax.get_figure().canvas.mpl_connect("resize_event", callback) ax.callbacks.connect("xlim_changed", callback) ax.callbacks.connect("ylim_changed", callback)
def draw(self, graph, filename, *args, **kwds): # Some abbreviations for sake of simplicity directed = graph.is_directed() # 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) unit = kwds.get("unit", ('px','px')) if isinstance(unit,tuple): px = UnitConverter(unit[0],'px') cm = UnitConverter(unit[0],'cm') pt = UnitConverter(unit[1],'pt') else: px = UnitConverter(unit,'px') cm = UnitConverter(unit,'cm') pt = UnitConverter(unit,'pt') px2cm = UnitConverter('px','cm') # Contract the drawing area by the margin and fit the layout box = kwds.get("bbox", (600,600)) self.bbox = igraph.drawing.utils.BoundingBox(px.conv(box[0]),px.conv(box[1])) 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) def curve_conv(curved): if curved == 0: return 0 else: v1 = np.array([0,0]) v2 = np.array([1,1]) v3 = np.array([(2*v1[0]+v2[0]) / 3.0 - curved * 0.5 * (v2[1]-v1[1]), (2*v1[1]+v2[1]) / 3.0 + curved * 0.5 * (v2[0]-v1[0]) ]) vec1 = v2-v1 vec2 = v3 -v1 angle = np.rad2deg(np.arccos(np.dot(vec1,vec2) / np.sqrt((vec1*vec1).sum()) / np.sqrt((vec2*vec2).sum()))) return np.round(np.sign(curved) * angle * -1,self.digit) # Custom color converter function def color_conv(color): if not color is "": rgba = color_name_to_rgba(color) RGB = [str(int(rgba[0]*255)), str(int(rgba[1]*255)), str(int(rgba[2]*255))] color = '{'+'.,'.join(RGB)+'}' return color # Custom label size converter function def label_size_conv(label_size): if not label_size is "": return np.round(pt.conv(label_size)/7,self.digit) # Construct the visual vertex/edge builders class VisualVertexBuilder(AttributeCollectorBase): """Collects some visual properties of a vertex for drawing""" _kwds_prefix = "vertex_" size = str(self.vertex_defaults["size"]) color = (str(self.vertex_defaults["color"]), color_conv) opacity = str(self.vertex_defaults["opacity"]) label = str(self.vertex_defaults["label"]) label_position = str(self.vertex_defaults["label_position"]) label_distance = str(self.vertex_defaults["label_distance"]) label_color = (str(self.vertex_defaults["label_color"]), color_conv) label_size = (str(self.vertex_defaults["label_size"]),label_size_conv) shape = str(self.vertex_defaults["shape"]) style = str(self.vertex_defaults["style"]) layer = str(self.vertex_defaults["layer"]) class VisualEdgeBuilder(AttributeCollectorBase): """Collects some visual properties of an edge for drawing""" _kwds_prefix = "edge_" width = str(self.edge_defaults["width"]) color = (str(self.edge_defaults["color"]), color_conv) opacity = str(self.edge_defaults["opacity"]) curved = (str(self.edge_defaults["curved"]), curve_conv) label = str(self.edge_defaults["label"]) label_position = str(self.edge_defaults["label_position"]) label_distance = str(self.edge_defaults["label_distance"]) label_color = (str(self.edge_defaults["label_color"]), color_conv) label_size = (str(self.edge_defaults["label_size"]),label_size_conv) style = str(self.edge_defaults["style"]) arrow_size = str(self.edge_defaults["arrow_size"]) arrow_width = str(self.edge_defaults["arrow_width"]) vertex_builder = VisualVertexBuilder(graph.vs, kwds) edge_builder = VisualEdgeBuilder(graph.es, kwds) # Create Vertices if "vertex_id" in kwds: vertex_ids = kwds["vertex_id"] if isinstance(vertex_ids, str): vertex_ids = graph.vs[vertex_id] else: vertex_ids = range(graph.vcount()) vertex_ids = [str(identifier) for identifier in vertex_ids] self.vertices = [] for vertex_id, vertex, coords in zip(vertex_ids, vertex_builder,layout): v = [] v.append('x='+str(px2cm.conv(coords[0]))) if not coords[0] is "" else None v.append('y='+str(-px2cm.conv(coords[1]))) if not coords[1] is "" else None v.append('size='+str(cm.conv(vertex.size))) if not vertex.size is "" else None v.append('color='+vertex.color) if not vertex.color is "" else None v.append('opacity='+vertex.opacity) if not vertex.opacity is "" else None v.append('label='+vertex.label) if not vertex.label is "" else None v.append('position='+vertex.label_position) if not vertex.label_position is "" else None v.append('distance='+str(cm.conv(vertex.label_distance))) if not vertex.label_distance is "" else None v.append('fontcolor='+vertex.label_color) if not vertex.label_color is "" else None v.append('fontscale='+str(vertex.label_size)) if not vertex.label_size is None else None v.append('shape='+vertex.shape) if not vertex.shape is "" else None v.append('style={'+vertex.style+'}') if not vertex.style is "" else None v.append('layer='+str(vertex.layer)) if not vertex.layer is "" else None v.append('RGB') if not vertex.color is "" else None self.vertices.append([vertex_id,v]) # Create Edges edge_ids = [] for v1, v2 in graph.get_edgelist(): edge_ids.append((vertex_ids[v1],vertex_ids[v2])) self.edges = [] for edge_id, edge in zip(edge_ids, edge_builder): e = [] e.append('lw='+str(pt.conv(edge.width))) if not edge.width is "" else None e.append('color='+edge.color) if not edge.color is "" else None e.append('opacity='+edge.opacity) if not edge.opacity is "" else None e.append('bend='+str(edge.curved)) if not edge.curved is 0 else None e.append('label='+edge.label) if not edge.label is "" else None e.append('position='+edge.label_position) if not edge.label_position is "" else None e.append('distance='+edge.label_distance) if not edge.label_distance is "" else None e.append('fontcolor='+edge.label_color) if not edge.label_color is "" else None e.append('fontscale='+str(edge.label_size)) if not edge.label_size is None else None a = [] a.append('length='+str(15*cm.conv(edge.arrow_size))+'cm') if not edge.arrow_size is "" else None a.append('width='+str(10*cm.conv(edge.arrow_width))+'cm') if not edge.arrow_width is "" else None e.append('style={-{Latex['+', '.join(a)+']}, '+edge.style+'}') if len(a) > 0 else None e.append('Direct') if directed else None e.append('RGB') if not edge.color is "" else None self.edges.append([edge_id[0],edge_id[1],e]) latex_header = ['\\documentclass{standalone}\n', '\\usepackage{tikz-network}\n', '\\begin{document}\n', '\\begin{tikzpicture}\n'] if "3d" in kwds: latex_header.append('[multilayer=3d]\n') elif 'vertex_layer' in kwds: latex_header.append('[multilayer]\n') with open(filename, 'w') as out: out.write("".join(latex_header)) for vertex_id, args in self.vertices: out.write("\\Vertex["+", ".join(args)+"]{"+vertex_id + "}\n") for v1, v2, args in self.edges: out.write("\\Edge["+", ".join(args)+"]("+v1+")("+v2+")\n") out.write("\\end{tikzpicture}\n\\end{document}") pass
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 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)