Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
    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
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    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)