Пример #1
0
    def _get_elevation(self, u, v):
        """
        Get the angle of elevation from the source node to the target node.
        The angle of elevation considers the aspect ratio.

        :param u: The source node's position as a tuple.
        :type u: tuple
        :param v: The target node's position as a tuple.
        :type v: tuple

        :return: The angle of elevation between the source and target nodes in radians.
        :rtype: float
        """

        if (u[0] == v[0] and u[1] == v[1]):
            return 0

        xdiff = v[0] - u[0]
        if xdiff == 0:
            return math.pi / 2.

        ratio = util.get_aspect(self.drawable.axes)
        ydiff = (v[1] - u[1]) * ratio

        return math.atan(ydiff / xdiff)
Пример #2
0
    def _draw_edge_names(self, edges, nodes, positions, s, *args, **kwargs):
        """
        Draw names for the edges.
        Names are drawn if they have a `name` attribute.
        The `name_style` attribute, if given, is used to override the default name style.
        Names are written left-to-right.

        Any additional keyword arguments are considered to be styling options.

        :param edges: The list of edges for which to draw names.
        :type edges: :class:`networkx.classes.reportviews.NodeView`
        :param nodes: The list of nodes in the graph.
                      They are used when rendering names for looped edges.
        :type nodes: networkx.classes.reportviews.NodeView
        :param positions: The positions of the nodes as a dictionary.
                          The keys are the node names, and the values are the corresponding positions.
        :type positions: dict
        :param s: The default radius of the node.
                  It may be overwritten with the node's own radius.
        :type s: float

        :return: A dictionary of rendered edge names.
                 The keys are the edge names and the values are :class:`~text.annotation.Annotation`, representing the rendered annotations.
        :rtype: dict
        """

        annotations = {}

        for (source, target) in edges:
            """
            Nodes are drawn only if they have a name attribute.
            """
            name = edges[(source, target)].get('name')
            if name:
                """
                By default, edge names are aligned centrally.
                However, the style can be overriden by providing a `name_style` attribute.
                """
                default_style = {
                    'align': 'center',
                    'ha': 'left',
                    'va': 'center'
                }
                default_style.update(**kwargs)
                style = edges[(source, target)].get('name_style', {})
                default_style.update(style)
                """
                To draw the name from left to right, order the source and target nodes accordingly.
                """
                u, v = sorted([positions[source], positions[target]],
                              key=lambda node: node[0])
                """
                Draw the annotation to get an idea of its width and remove it immediately.
                """
                annotation = Annotation(self.drawable)
                annotation.draw([name], (u[0]), u[1], **default_style)
                bb = annotation.get_virtual_bb()
                annotation.remove()

                if source == target:
                    """
                    If the edge is a loop, draw the name at the apex.
                    """
                    radius = self._get_radius(nodes[source],
                                              s=nodes[source].get('style',
                                                                  {}).get(
                                                                      's', s))
                    annotation = Annotation(self.drawable)
                    x = (u[0] - bb.width / 2., u[0] + bb.width / 2.)
                    y = u[1] + radius[1] * 2 + bb.height
                    annotation.draw([name], x, y, **default_style)
                    continue
                """
                Re-draw the annotation, this time positionally centered along the edge.
                The rotation depends on the elevation from the source to the target node.
                """
                ratio = util.get_aspect(self.drawable.axes)
                distance = self._get_distance(u, v)
                direction = self._get_direction(u, v)
                angle = self._get_elevation(u, v)
                """
                The annotation's x-position is bound rigidly based on the width of the annotation.
                The y-position is based on the angle. This is because the horizontal alignment is always set to `left`.
                """
                annotation = Annotation(self.drawable)
                x = (u[0] + direction[0] * distance / 2. - bb.width / 2.,
                     u[0] + direction[0] * distance / 2. + bb.width / 2.)
                y = u[1] + direction[
                    1] * distance / 2. + bb.height / 2. * math.sin(angle) * (
                        math.degrees(angle) > 0)
                annotation.draw([name],
                                x,
                                y,
                                rotation=math.degrees(angle),
                                **default_style)
                annotations[(source, target)] = annotation

        return annotations
Пример #3
0
    def _draw_loop(self,
                   node,
                   position,
                   s,
                   directed,
                   offset_angle=math.pi / 2,
                   *args,
                   **kwargs):
        """
        Draw a loop, indicating an edge from a node to itself.

        Any additional arguments and keyword arguments are passed on to the edge drawing functions.

        :param node: The node for which to draw a loop.
        :type node: dict
        :param position: The position of the node as a tuple.
        :type position: tuple
        :param s: The default radius of the node.
                  It may be overwritten with the node's own radius.
        :type s: float
        :param directed: A boolean indicating whether the graph is directed or not.
        :type: bool
        :param offset_angle: The loop's offset angle in radians.
                             By default, the value is :math:`\\frac{\\pi}{2}`, which places the loop on top of the node.
        :type offset_angle: float

        :return: A tuple containing the undirected edge and, if directed, the arrow.
        :rtype: tuple
        """
        """
        Get the node's radius and calculate the loop's radius as a fraction of the node's radius.
        To calculate the angles covered by the loop, the loop is initally placed directly above the node.
        Since nodes are circular, the loop's position is just rotated around the node later.
        """
        radius = self._get_radius(node, s)
        loop = (radius[0] * 0.5, radius[1] * 0.5)
        center = (position[0], position[1] + radius[1])
        """
        Get the vertical distance from the node to the place where the loop will intersect with it.
        The notation is taken from `this blog post <https://diego.assencio.com/?index=8d6ca3d82151bad815f78addf9b5c1c6>`_.
        """
        d = center[1] - position[1]
        d1 = (radius[1]**2 - loop[1]**2 + d**2) / (2 * d)
        d2 = d - d1
        """
        Calculate the horizontal distance from the center of the node to where the node and the loop intersect.
        """
        ratio = util.get_aspect(self.drawable.axes)
        x1 = math.sqrt(((radius[0])**2 * radius[1]**2 -
                        (radius[0])**2 * d1**2) / ((radius[0])**2)) * ratio
        """
        Offset the center of the node properly, this time based on the given offset angle.
        Calculate the angle (in degrees) from the rightmost intersection to the leftmost intersection.
        Use it to calculate the x and y coordinates of the points of the looped edge.
        """
        center = (position[0] + radius[0] * math.cos(offset_angle),
                  position[1] + radius[1] * math.sin(offset_angle))
        angle = math.asin(-d2 / loop[1])
        angle = math.floor(math.degrees(angle))
        x = [
            center[0] + loop[0] * math.cos(math.pi *
                                           (i / 180 - 1 / 2) + offset_angle)
            for i in range(angle, -angle + 180)
        ]
        y = [
            center[1] + loop[1] * math.sin(math.pi *
                                           (i / 180 - 1 / 2) + offset_angle)
            for i in range(angle, -angle + 180)
        ]
        """
        Remove some style attributes that belong to arrows, not edges.
        """
        edge_style = dict(kwargs)
        edge_style.pop('headwidth', None)
        edge_style.pop('headlength', None)
        edge_style['linewidth'] = edge_style.get('linewidth', 1) * 2
        edge = self.drawable.plot(x, y, zorder=-1, **edge_style)
        """
        If the arrow is directed, calculate its position.
        The arrow points is set to always point downwards.
        """
        if directed:
            arrowprops = dict(kwargs)
            if 'headwidth' in arrowprops:
                arrowprops['headwidth'] = arrowprops.get('headwidth') * 0.75
            if 'headlength' in arrowprops:
                arrowprops['headlength'] = arrowprops.get('headwidth') * 0.75
            xy = (x[-1], y[-1])
            xytext = (x[-2], y[-2])
            arrow = self.drawable.axes.annotate('',
                                                xy=xy,
                                                xytext=xytext,
                                                zorder=-1,
                                                arrowprops=arrowprops)

        return (edge, arrow) if directed else (edge, )
Пример #4
0
    def _draw_edges(self,
                    edges,
                    nodes,
                    positions,
                    s,
                    directed=False,
                    *args,
                    **kwargs):
        """
        Draw the edges connecting the given nodes.
        Depending on whether the graph is undirected or directed, the edges are drawn with arrows.

        Any additional arguments and keyword arguments are passed on to the `matplotlib.pyplot.plot <https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.pyplot.plot.html>`_ function or as arrowprops.

        :param edges: The list of edges to draw.
                      The edges should be a list of tuples representing the source and target.
        :type edges: list of tuple
        :param nodes: The list of graph nodes.
                      These are not the rendered nodes, but the graph nodes.
        :type nodes: networkx.classes.reportviews.NodeView
        :param positions: The positions of the nodes as a dictionary.
                          The keys are the node names, and the values are the corresponding positions.
                          They are used to connect the edges together.
        :type positions: dict
        :param s: The default radius of the node.
                  It may be overwritten with the node's own radius.
        :type s: float
        :param directed: A boolean indicating whether the graph is directed or not.
        :type directed: bool

        :return: A list of drawn edges.
                 If the graph is undirected, lines are returned.
                 Otherwise, annotations (with arrows) are returned.
        :rtype: list of :class:`matplotlib.lines.Line2D` or list of :class:`matplotlib.text.Annotation`
        """

        rendered = {}

        for source, target in edges:
            if source == target:
                rendered[(source, target)] = self._draw_loop(
                    nodes[target],
                    positions[target],
                    s=nodes[target].get('style', {}).get('s', s),
                    directed=directed,
                    *args,
                    **kwargs)
                continue
            """
            Load the edge's style.
            The keyword arguments may be overwritten by the edge's style.
            """
            u, v = list(positions[source]), list(positions[target])
            edge_style = dict(kwargs)
            edge_style.update(edges[(source, target)].get('style', {}))
            """
            Update the start and end positions so that the edges do not start and end at the center of the nodes.
            This allows nodes to be transparent without the edges looking bad.
            This is done by calculating the radius of the nodes, which is where the edges should end.
            Calculate the distance between the two centers and reduce from it the radius of the source and target nodes.
            """
            ratio = util.get_aspect(self.drawable.axes)
            angle = self._get_angle(u, v)
            direction = self._get_direction(u, v)
            for node, position in zip([source, target], [u, v]):
                distance = self._get_distance(
                    u, v
                )  # since positions are changing, the distance has to be computed each time
                radius = self._get_radius(nodes[node],
                                          s=nodes[node].get('style',
                                                            {}).get('s', s))
                if ratio > 1:
                    diff = abs(radius[0] * math.cos(angle)) / ratio + abs(
                        radius[1] * math.sin(angle))
                else:
                    diff = abs(radius[0] * math.cos(angle)) + abs(
                        radius[1] * math.sin(angle)) * ratio
                """
                Retract the start and end by the radius.
                """
                if node == source:
                    u = [
                        u[0] + direction[0] * diff, u[1] + direction[1] * diff
                    ]
                elif node == target:
                    v = [
                        u[0] + direction[0] * (distance - diff),
                        u[1] + direction[1] * (distance - diff)
                    ]

            if not directed:
                """
                If the graph is not directed, connect the two nodes' centers with a straight line.
                """
                x, y = (u[0], v[0]), (u[1], v[1])
                rendered[(source,
                          target)] = self.drawable.plot(x,
                                                        y,
                                                        zorder=-1,
                                                        *args,
                                                        **edge_style)[0]
            if directed:
                rendered[(source, target)] = self.drawable.axes.annotate(
                    '', xy=v, xytext=u, zorder=-1, arrowprops=edge_style)

        return rendered