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_path(self, points=None, corner_radius=0): """Sets up a Cairo path for the outline of a polygon on the given Cairo context. @param points: the coordinates of the corners of the polygon, in clockwise or counter-clockwise order, or C{None} if we are about to use the C{points} property of the class. @param corner_radius: if zero, an ordinary polygon will be drawn. If positive, the corners of the polygon will be rounded with the given radius. """ if points is None: points = self.points self.context.new_path() if len(points) < 2: # Well, a polygon must have at least two corner points return ctx = self.context if corner_radius <= 0: # No rounded corners, this is simple ctx.move_to(*points[-1]) for point in points: ctx.line_to(*point) return # Rounded corners. First, we will take each side of the # polygon and find what the corner radius should be on # each corner. If the side is longer than 2r (where r is # equal to corner_radius), the radius allowed by that side # is r; if the side is shorter, the radius is the length # of the side / 2. For each corner, the final corner radius # is the smaller of the radii on the two sides adjacent to # the corner. points = [Point(*point) for point in points] side_vecs = [ v - u for u, v in consecutive_pairs(points, circular=True) ] half_side_lengths = [side.length() / 2 for side in side_vecs] corner_radii = [corner_radius] * len(points) for idx in range(len(corner_radii)): prev_idx = -1 if idx == 0 else idx - 1 radii = [ corner_radius, half_side_lengths[prev_idx], half_side_lengths[idx] ] corner_radii[idx] = min(radii) # Okay, move to the last corner, adjusted by corner_radii[-1] # towards the first corner ctx.move_to(*(points[-1].towards(points[0], corner_radii[-1]))) # Now, for each point in points, draw a line towards the # corner, stopping before it in a distance of corner_radii[idx], # then draw the corner u = points[-1] for idx, (v, w) in enumerate(consecutive_pairs(points, True)): radius = corner_radii[idx] ctx.line_to(*v.towards(u, radius)) aux1 = v.towards(u, radius / 2) aux2 = v.towards(w, radius / 2) ctx.curve_to(aux1.x, aux1.y, aux2.x, aux2.y, *v.towards(w, corner_radii[idx])) u = v
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)