You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			815 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			815 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			Python
		
	
"""
 | 
						|
Module for creating Sankey diagrams using Matplotlib.
 | 
						|
"""
 | 
						|
 | 
						|
import logging
 | 
						|
from types import SimpleNamespace
 | 
						|
 | 
						|
import numpy as np
 | 
						|
 | 
						|
import matplotlib as mpl
 | 
						|
from matplotlib.path import Path
 | 
						|
from matplotlib.patches import PathPatch
 | 
						|
from matplotlib.transforms import Affine2D
 | 
						|
from matplotlib import _docstring
 | 
						|
 | 
						|
_log = logging.getLogger(__name__)
 | 
						|
 | 
						|
__author__ = "Kevin L. Davies"
 | 
						|
__credits__ = ["Yannick Copin"]
 | 
						|
__license__ = "BSD"
 | 
						|
__version__ = "2011/09/16"
 | 
						|
 | 
						|
# Angles [deg/90]
 | 
						|
RIGHT = 0
 | 
						|
UP = 1
 | 
						|
# LEFT = 2
 | 
						|
DOWN = 3
 | 
						|
 | 
						|
 | 
						|
class Sankey:
 | 
						|
    """
 | 
						|
    Sankey diagram.
 | 
						|
 | 
						|
      Sankey diagrams are a specific type of flow diagram, in which
 | 
						|
      the width of the arrows is shown proportionally to the flow
 | 
						|
      quantity.  They are typically used to visualize energy or
 | 
						|
      material or cost transfers between processes.
 | 
						|
      `Wikipedia (6/1/2011) <https://en.wikipedia.org/wiki/Sankey_diagram>`_
 | 
						|
 | 
						|
    """
 | 
						|
 | 
						|
    def __init__(self, ax=None, scale=1.0, unit='', format='%G', gap=0.25,
 | 
						|
                 radius=0.1, shoulder=0.03, offset=0.15, head_angle=100,
 | 
						|
                 margin=0.4, tolerance=1e-6, **kwargs):
 | 
						|
        """
 | 
						|
        Create a new Sankey instance.
 | 
						|
 | 
						|
        The optional arguments listed below are applied to all subdiagrams so
 | 
						|
        that there is consistent alignment and formatting.
 | 
						|
 | 
						|
        In order to draw a complex Sankey diagram, create an instance of
 | 
						|
        `Sankey` by calling it without any kwargs::
 | 
						|
 | 
						|
            sankey = Sankey()
 | 
						|
 | 
						|
        Then add simple Sankey sub-diagrams::
 | 
						|
 | 
						|
            sankey.add() # 1
 | 
						|
            sankey.add() # 2
 | 
						|
            #...
 | 
						|
            sankey.add() # n
 | 
						|
 | 
						|
        Finally, create the full diagram::
 | 
						|
 | 
						|
            sankey.finish()
 | 
						|
 | 
						|
        Or, instead, simply daisy-chain those calls::
 | 
						|
 | 
						|
            Sankey().add().add...  .add().finish()
 | 
						|
 | 
						|
        Other Parameters
 | 
						|
        ----------------
 | 
						|
        ax : `~matplotlib.axes.Axes`
 | 
						|
            Axes onto which the data should be plotted.  If *ax* isn't
 | 
						|
            provided, new Axes will be created.
 | 
						|
        scale : float
 | 
						|
            Scaling factor for the flows.  *scale* sizes the width of the paths
 | 
						|
            in order to maintain proper layout.  The same scale is applied to
 | 
						|
            all subdiagrams.  The value should be chosen such that the product
 | 
						|
            of the scale and the sum of the inputs is approximately 1.0 (and
 | 
						|
            the product of the scale and the sum of the outputs is
 | 
						|
            approximately -1.0).
 | 
						|
        unit : str
 | 
						|
            The physical unit associated with the flow quantities.  If *unit*
 | 
						|
            is None, then none of the quantities are labeled.
 | 
						|
        format : str or callable
 | 
						|
            A Python number formatting string or callable used to label the
 | 
						|
            flows with their quantities (i.e., a number times a unit, where the
 | 
						|
            unit is given). If a format string is given, the label will be
 | 
						|
            ``format % quantity``. If a callable is given, it will be called
 | 
						|
            with ``quantity`` as an argument.
 | 
						|
        gap : float
 | 
						|
            Space between paths that break in/break away to/from the top or
 | 
						|
            bottom.
 | 
						|
        radius : float
 | 
						|
            Inner radius of the vertical paths.
 | 
						|
        shoulder : float
 | 
						|
            Size of the shoulders of output arrows.
 | 
						|
        offset : float
 | 
						|
            Text offset (from the dip or tip of the arrow).
 | 
						|
        head_angle : float
 | 
						|
            Angle, in degrees, of the arrow heads (and negative of the angle of
 | 
						|
            the tails).
 | 
						|
        margin : float
 | 
						|
            Minimum space between Sankey outlines and the edge of the plot
 | 
						|
            area.
 | 
						|
        tolerance : float
 | 
						|
            Acceptable maximum of the magnitude of the sum of flows.  The
 | 
						|
            magnitude of the sum of connected flows cannot be greater than
 | 
						|
            *tolerance*.
 | 
						|
        **kwargs
 | 
						|
            Any additional keyword arguments will be passed to `add`, which
 | 
						|
            will create the first subdiagram.
 | 
						|
 | 
						|
        See Also
 | 
						|
        --------
 | 
						|
        Sankey.add
 | 
						|
        Sankey.finish
 | 
						|
 | 
						|
        Examples
 | 
						|
        --------
 | 
						|
        .. plot:: gallery/specialty_plots/sankey_basics.py
 | 
						|
        """
 | 
						|
        # Check the arguments.
 | 
						|
        if gap < 0:
 | 
						|
            raise ValueError(
 | 
						|
                "'gap' is negative, which is not allowed because it would "
 | 
						|
                "cause the paths to overlap")
 | 
						|
        if radius > gap:
 | 
						|
            raise ValueError(
 | 
						|
                "'radius' is greater than 'gap', which is not allowed because "
 | 
						|
                "it would cause the paths to overlap")
 | 
						|
        if head_angle < 0:
 | 
						|
            raise ValueError(
 | 
						|
                "'head_angle' is negative, which is not allowed because it "
 | 
						|
                "would cause inputs to look like outputs and vice versa")
 | 
						|
        if tolerance < 0:
 | 
						|
            raise ValueError(
 | 
						|
                "'tolerance' is negative, but it must be a magnitude")
 | 
						|
 | 
						|
        # Create Axes if necessary.
 | 
						|
        if ax is None:
 | 
						|
            import matplotlib.pyplot as plt
 | 
						|
            fig = plt.figure()
 | 
						|
            ax = fig.add_subplot(1, 1, 1, xticks=[], yticks=[])
 | 
						|
 | 
						|
        self.diagrams = []
 | 
						|
 | 
						|
        # Store the inputs.
 | 
						|
        self.ax = ax
 | 
						|
        self.unit = unit
 | 
						|
        self.format = format
 | 
						|
        self.scale = scale
 | 
						|
        self.gap = gap
 | 
						|
        self.radius = radius
 | 
						|
        self.shoulder = shoulder
 | 
						|
        self.offset = offset
 | 
						|
        self.margin = margin
 | 
						|
        self.pitch = np.tan(np.pi * (1 - head_angle / 180.0) / 2.0)
 | 
						|
        self.tolerance = tolerance
 | 
						|
 | 
						|
        # Initialize the vertices of tight box around the diagram(s).
 | 
						|
        self.extent = np.array((np.inf, -np.inf, np.inf, -np.inf))
 | 
						|
 | 
						|
        # If there are any kwargs, create the first subdiagram.
 | 
						|
        if len(kwargs):
 | 
						|
            self.add(**kwargs)
 | 
						|
 | 
						|
    def _arc(self, quadrant=0, cw=True, radius=1, center=(0, 0)):
 | 
						|
        """
 | 
						|
        Return the codes and vertices for a rotated, scaled, and translated
 | 
						|
        90 degree arc.
 | 
						|
 | 
						|
        Other Parameters
 | 
						|
        ----------------
 | 
						|
        quadrant : {0, 1, 2, 3}, default: 0
 | 
						|
            Uses 0-based indexing (0, 1, 2, or 3).
 | 
						|
        cw : bool, default: True
 | 
						|
            If True, the arc vertices are produced clockwise; counter-clockwise
 | 
						|
            otherwise.
 | 
						|
        radius : float, default: 1
 | 
						|
            The radius of the arc.
 | 
						|
        center : (float, float), default: (0, 0)
 | 
						|
            (x, y) tuple of the arc's center.
 | 
						|
        """
 | 
						|
        # Note:  It would be possible to use matplotlib's transforms to rotate,
 | 
						|
        # scale, and translate the arc, but since the angles are discrete,
 | 
						|
        # it's just as easy and maybe more efficient to do it here.
 | 
						|
        ARC_CODES = [Path.LINETO,
 | 
						|
                     Path.CURVE4,
 | 
						|
                     Path.CURVE4,
 | 
						|
                     Path.CURVE4,
 | 
						|
                     Path.CURVE4,
 | 
						|
                     Path.CURVE4,
 | 
						|
                     Path.CURVE4]
 | 
						|
        # Vertices of a cubic Bezier curve approximating a 90 deg arc
 | 
						|
        # These can be determined by Path.arc(0, 90).
 | 
						|
        ARC_VERTICES = np.array([[1.00000000e+00, 0.00000000e+00],
 | 
						|
                                 [1.00000000e+00, 2.65114773e-01],
 | 
						|
                                 [8.94571235e-01, 5.19642327e-01],
 | 
						|
                                 [7.07106781e-01, 7.07106781e-01],
 | 
						|
                                 [5.19642327e-01, 8.94571235e-01],
 | 
						|
                                 [2.65114773e-01, 1.00000000e+00],
 | 
						|
                                 # Insignificant
 | 
						|
                                 # [6.12303177e-17, 1.00000000e+00]])
 | 
						|
                                 [0.00000000e+00, 1.00000000e+00]])
 | 
						|
        if quadrant in (0, 2):
 | 
						|
            if cw:
 | 
						|
                vertices = ARC_VERTICES
 | 
						|
            else:
 | 
						|
                vertices = ARC_VERTICES[:, ::-1]  # Swap x and y.
 | 
						|
        else:  # 1, 3
 | 
						|
            # Negate x.
 | 
						|
            if cw:
 | 
						|
                # Swap x and y.
 | 
						|
                vertices = np.column_stack((-ARC_VERTICES[:, 1],
 | 
						|
                                             ARC_VERTICES[:, 0]))
 | 
						|
            else:
 | 
						|
                vertices = np.column_stack((-ARC_VERTICES[:, 0],
 | 
						|
                                             ARC_VERTICES[:, 1]))
 | 
						|
        if quadrant > 1:
 | 
						|
            radius = -radius  # Rotate 180 deg.
 | 
						|
        return list(zip(ARC_CODES, radius * vertices +
 | 
						|
                        np.tile(center, (ARC_VERTICES.shape[0], 1))))
 | 
						|
 | 
						|
    def _add_input(self, path, angle, flow, length):
 | 
						|
        """
 | 
						|
        Add an input to a path and return its tip and label locations.
 | 
						|
        """
 | 
						|
        if angle is None:
 | 
						|
            return [0, 0], [0, 0]
 | 
						|
        else:
 | 
						|
            x, y = path[-1][1]  # Use the last point as a reference.
 | 
						|
            dipdepth = (flow / 2) * self.pitch
 | 
						|
            if angle == RIGHT:
 | 
						|
                x -= length
 | 
						|
                dip = [x + dipdepth, y + flow / 2.0]
 | 
						|
                path.extend([(Path.LINETO, [x, y]),
 | 
						|
                             (Path.LINETO, dip),
 | 
						|
                             (Path.LINETO, [x, y + flow]),
 | 
						|
                             (Path.LINETO, [x + self.gap, y + flow])])
 | 
						|
                label_location = [dip[0] - self.offset, dip[1]]
 | 
						|
            else:  # Vertical
 | 
						|
                x -= self.gap
 | 
						|
                if angle == UP:
 | 
						|
                    sign = 1
 | 
						|
                else:
 | 
						|
                    sign = -1
 | 
						|
 | 
						|
                dip = [x - flow / 2, y - sign * (length - dipdepth)]
 | 
						|
                if angle == DOWN:
 | 
						|
                    quadrant = 2
 | 
						|
                else:
 | 
						|
                    quadrant = 1
 | 
						|
 | 
						|
                # Inner arc isn't needed if inner radius is zero
 | 
						|
                if self.radius:
 | 
						|
                    path.extend(self._arc(quadrant=quadrant,
 | 
						|
                                          cw=angle == UP,
 | 
						|
                                          radius=self.radius,
 | 
						|
                                          center=(x + self.radius,
 | 
						|
                                                  y - sign * self.radius)))
 | 
						|
                else:
 | 
						|
                    path.append((Path.LINETO, [x, y]))
 | 
						|
                path.extend([(Path.LINETO, [x, y - sign * length]),
 | 
						|
                             (Path.LINETO, dip),
 | 
						|
                             (Path.LINETO, [x - flow, y - sign * length])])
 | 
						|
                path.extend(self._arc(quadrant=quadrant,
 | 
						|
                                      cw=angle == DOWN,
 | 
						|
                                      radius=flow + self.radius,
 | 
						|
                                      center=(x + self.radius,
 | 
						|
                                              y - sign * self.radius)))
 | 
						|
                path.append((Path.LINETO, [x - flow, y + sign * flow]))
 | 
						|
                label_location = [dip[0], dip[1] - sign * self.offset]
 | 
						|
 | 
						|
            return dip, label_location
 | 
						|
 | 
						|
    def _add_output(self, path, angle, flow, length):
 | 
						|
        """
 | 
						|
        Append an output to a path and return its tip and label locations.
 | 
						|
 | 
						|
        .. note:: *flow* is negative for an output.
 | 
						|
        """
 | 
						|
        if angle is None:
 | 
						|
            return [0, 0], [0, 0]
 | 
						|
        else:
 | 
						|
            x, y = path[-1][1]  # Use the last point as a reference.
 | 
						|
            tipheight = (self.shoulder - flow / 2) * self.pitch
 | 
						|
            if angle == RIGHT:
 | 
						|
                x += length
 | 
						|
                tip = [x + tipheight, y + flow / 2.0]
 | 
						|
                path.extend([(Path.LINETO, [x, y]),
 | 
						|
                             (Path.LINETO, [x, y + self.shoulder]),
 | 
						|
                             (Path.LINETO, tip),
 | 
						|
                             (Path.LINETO, [x, y - self.shoulder + flow]),
 | 
						|
                             (Path.LINETO, [x, y + flow]),
 | 
						|
                             (Path.LINETO, [x - self.gap, y + flow])])
 | 
						|
                label_location = [tip[0] + self.offset, tip[1]]
 | 
						|
            else:  # Vertical
 | 
						|
                x += self.gap
 | 
						|
                if angle == UP:
 | 
						|
                    sign, quadrant = 1, 3
 | 
						|
                else:
 | 
						|
                    sign, quadrant = -1, 0
 | 
						|
 | 
						|
                tip = [x - flow / 2.0, y + sign * (length + tipheight)]
 | 
						|
                # Inner arc isn't needed if inner radius is zero
 | 
						|
                if self.radius:
 | 
						|
                    path.extend(self._arc(quadrant=quadrant,
 | 
						|
                                          cw=angle == UP,
 | 
						|
                                          radius=self.radius,
 | 
						|
                                          center=(x - self.radius,
 | 
						|
                                                  y + sign * self.radius)))
 | 
						|
                else:
 | 
						|
                    path.append((Path.LINETO, [x, y]))
 | 
						|
                path.extend([(Path.LINETO, [x, y + sign * length]),
 | 
						|
                             (Path.LINETO, [x - self.shoulder,
 | 
						|
                                            y + sign * length]),
 | 
						|
                             (Path.LINETO, tip),
 | 
						|
                             (Path.LINETO, [x + self.shoulder - flow,
 | 
						|
                                            y + sign * length]),
 | 
						|
                             (Path.LINETO, [x - flow, y + sign * length])])
 | 
						|
                path.extend(self._arc(quadrant=quadrant,
 | 
						|
                                      cw=angle == DOWN,
 | 
						|
                                      radius=self.radius - flow,
 | 
						|
                                      center=(x - self.radius,
 | 
						|
                                              y + sign * self.radius)))
 | 
						|
                path.append((Path.LINETO, [x - flow, y + sign * flow]))
 | 
						|
                label_location = [tip[0], tip[1] + sign * self.offset]
 | 
						|
            return tip, label_location
 | 
						|
 | 
						|
    def _revert(self, path, first_action=Path.LINETO):
 | 
						|
        """
 | 
						|
        A path is not simply reversible by path[::-1] since the code
 | 
						|
        specifies an action to take from the **previous** point.
 | 
						|
        """
 | 
						|
        reverse_path = []
 | 
						|
        next_code = first_action
 | 
						|
        for code, position in path[::-1]:
 | 
						|
            reverse_path.append((next_code, position))
 | 
						|
            next_code = code
 | 
						|
        return reverse_path
 | 
						|
        # This might be more efficient, but it fails because 'tuple' object
 | 
						|
        # doesn't support item assignment:
 | 
						|
        # path[1] = path[1][-1:0:-1]
 | 
						|
        # path[1][0] = first_action
 | 
						|
        # path[2] = path[2][::-1]
 | 
						|
        # return path
 | 
						|
 | 
						|
    @_docstring.interpd
 | 
						|
    def add(self, patchlabel='', flows=None, orientations=None, labels='',
 | 
						|
            trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0),
 | 
						|
            rotation=0, **kwargs):
 | 
						|
        """
 | 
						|
        Add a simple Sankey diagram with flows at the same hierarchical level.
 | 
						|
 | 
						|
        Parameters
 | 
						|
        ----------
 | 
						|
        patchlabel : str
 | 
						|
            Label to be placed at the center of the diagram.
 | 
						|
            Note that *label* (not *patchlabel*) can be passed as keyword
 | 
						|
            argument to create an entry in the legend.
 | 
						|
 | 
						|
        flows : list of float
 | 
						|
            Array of flow values.  By convention, inputs are positive and
 | 
						|
            outputs are negative.
 | 
						|
 | 
						|
            Flows are placed along the top of the diagram from the inside out
 | 
						|
            in order of their index within *flows*.  They are placed along the
 | 
						|
            sides of the diagram from the top down and along the bottom from
 | 
						|
            the outside in.
 | 
						|
 | 
						|
            If the sum of the inputs and outputs is
 | 
						|
            nonzero, the discrepancy will appear as a cubic Bézier curve along
 | 
						|
            the top and bottom edges of the trunk.
 | 
						|
 | 
						|
        orientations : list of {-1, 0, 1}
 | 
						|
            List of orientations of the flows (or a single orientation to be
 | 
						|
            used for all flows).  Valid values are 0 (inputs from
 | 
						|
            the left, outputs to the right), 1 (from and to the top) or -1
 | 
						|
            (from and to the bottom).
 | 
						|
 | 
						|
        labels : list of (str or None)
 | 
						|
            List of labels for the flows (or a single label to be used for all
 | 
						|
            flows).  Each label may be *None* (no label), or a labeling string.
 | 
						|
            If an entry is a (possibly empty) string, then the quantity for the
 | 
						|
            corresponding flow will be shown below the string.  However, if
 | 
						|
            the *unit* of the main diagram is None, then quantities are never
 | 
						|
            shown, regardless of the value of this argument.
 | 
						|
 | 
						|
        trunklength : float
 | 
						|
            Length between the bases of the input and output groups (in
 | 
						|
            data-space units).
 | 
						|
 | 
						|
        pathlengths : list of float
 | 
						|
            List of lengths of the vertical arrows before break-in or after
 | 
						|
            break-away.  If a single value is given, then it will be applied to
 | 
						|
            the first (inside) paths on the top and bottom, and the length of
 | 
						|
            all other arrows will be justified accordingly.  The *pathlengths*
 | 
						|
            are not applied to the horizontal inputs and outputs.
 | 
						|
 | 
						|
        prior : int
 | 
						|
            Index of the prior diagram to which this diagram should be
 | 
						|
            connected.
 | 
						|
 | 
						|
        connect : (int, int)
 | 
						|
            A (prior, this) tuple indexing the flow of the prior diagram and
 | 
						|
            the flow of this diagram which should be connected.  If this is the
 | 
						|
            first diagram or *prior* is *None*, *connect* will be ignored.
 | 
						|
 | 
						|
        rotation : float
 | 
						|
            Angle of rotation of the diagram in degrees.  The interpretation of
 | 
						|
            the *orientations* argument will be rotated accordingly (e.g., if
 | 
						|
            *rotation* == 90, an *orientations* entry of 1 means to/from the
 | 
						|
            left).  *rotation* is ignored if this diagram is connected to an
 | 
						|
            existing one (using *prior* and *connect*).
 | 
						|
 | 
						|
        Returns
 | 
						|
        -------
 | 
						|
        Sankey
 | 
						|
            The current `.Sankey` instance.
 | 
						|
 | 
						|
        Other Parameters
 | 
						|
        ----------------
 | 
						|
        **kwargs
 | 
						|
           Additional keyword arguments set `matplotlib.patches.PathPatch`
 | 
						|
           properties, listed below.  For example, one may want to use
 | 
						|
           ``fill=False`` or ``label="A legend entry"``.
 | 
						|
 | 
						|
        %(Patch:kwdoc)s
 | 
						|
 | 
						|
        See Also
 | 
						|
        --------
 | 
						|
        Sankey.finish
 | 
						|
        """
 | 
						|
        # Check and preprocess the arguments.
 | 
						|
        flows = np.array([1.0, -1.0]) if flows is None else np.array(flows)
 | 
						|
        n = flows.shape[0]  # Number of flows
 | 
						|
        if rotation is None:
 | 
						|
            rotation = 0
 | 
						|
        else:
 | 
						|
            # In the code below, angles are expressed in deg/90.
 | 
						|
            rotation /= 90.0
 | 
						|
        if orientations is None:
 | 
						|
            orientations = 0
 | 
						|
        try:
 | 
						|
            orientations = np.broadcast_to(orientations, n)
 | 
						|
        except ValueError:
 | 
						|
            raise ValueError(
 | 
						|
                f"The shapes of 'flows' {np.shape(flows)} and 'orientations' "
 | 
						|
                f"{np.shape(orientations)} are incompatible"
 | 
						|
            ) from None
 | 
						|
        try:
 | 
						|
            labels = np.broadcast_to(labels, n)
 | 
						|
        except ValueError:
 | 
						|
            raise ValueError(
 | 
						|
                f"The shapes of 'flows' {np.shape(flows)} and 'labels' "
 | 
						|
                f"{np.shape(labels)} are incompatible"
 | 
						|
            ) from None
 | 
						|
        if trunklength < 0:
 | 
						|
            raise ValueError(
 | 
						|
                "'trunklength' is negative, which is not allowed because it "
 | 
						|
                "would cause poor layout")
 | 
						|
        if abs(np.sum(flows)) > self.tolerance:
 | 
						|
            _log.info("The sum of the flows is nonzero (%f; patchlabel=%r); "
 | 
						|
                      "is the system not at steady state?",
 | 
						|
                      np.sum(flows), patchlabel)
 | 
						|
        scaled_flows = self.scale * flows
 | 
						|
        gain = sum(max(flow, 0) for flow in scaled_flows)
 | 
						|
        loss = sum(min(flow, 0) for flow in scaled_flows)
 | 
						|
        if prior is not None:
 | 
						|
            if prior < 0:
 | 
						|
                raise ValueError("The index of the prior diagram is negative")
 | 
						|
            if min(connect) < 0:
 | 
						|
                raise ValueError(
 | 
						|
                    "At least one of the connection indices is negative")
 | 
						|
            if prior >= len(self.diagrams):
 | 
						|
                raise ValueError(
 | 
						|
                    f"The index of the prior diagram is {prior}, but there "
 | 
						|
                    f"are only {len(self.diagrams)} other diagrams")
 | 
						|
            if connect[0] >= len(self.diagrams[prior].flows):
 | 
						|
                raise ValueError(
 | 
						|
                    "The connection index to the source diagram is {}, but "
 | 
						|
                    "that diagram has only {} flows".format(
 | 
						|
                        connect[0], len(self.diagrams[prior].flows)))
 | 
						|
            if connect[1] >= n:
 | 
						|
                raise ValueError(
 | 
						|
                    f"The connection index to this diagram is {connect[1]}, "
 | 
						|
                    f"but this diagram has only {n} flows")
 | 
						|
            if self.diagrams[prior].angles[connect[0]] is None:
 | 
						|
                raise ValueError(
 | 
						|
                    f"The connection cannot be made, which may occur if the "
 | 
						|
                    f"magnitude of flow {connect[0]} of diagram {prior} is "
 | 
						|
                    f"less than the specified tolerance")
 | 
						|
            flow_error = (self.diagrams[prior].flows[connect[0]] +
 | 
						|
                          flows[connect[1]])
 | 
						|
            if abs(flow_error) >= self.tolerance:
 | 
						|
                raise ValueError(
 | 
						|
                    f"The scaled sum of the connected flows is {flow_error}, "
 | 
						|
                    f"which is not within the tolerance ({self.tolerance})")
 | 
						|
 | 
						|
        # Determine if the flows are inputs.
 | 
						|
        are_inputs = [None] * n
 | 
						|
        for i, flow in enumerate(flows):
 | 
						|
            if flow >= self.tolerance:
 | 
						|
                are_inputs[i] = True
 | 
						|
            elif flow <= -self.tolerance:
 | 
						|
                are_inputs[i] = False
 | 
						|
            else:
 | 
						|
                _log.info(
 | 
						|
                    "The magnitude of flow %d (%f) is below the tolerance "
 | 
						|
                    "(%f).\nIt will not be shown, and it cannot be used in a "
 | 
						|
                    "connection.", i, flow, self.tolerance)
 | 
						|
 | 
						|
        # Determine the angles of the arrows (before rotation).
 | 
						|
        angles = [None] * n
 | 
						|
        for i, (orient, is_input) in enumerate(zip(orientations, are_inputs)):
 | 
						|
            if orient == 1:
 | 
						|
                if is_input:
 | 
						|
                    angles[i] = DOWN
 | 
						|
                elif is_input is False:
 | 
						|
                    # Be specific since is_input can be None.
 | 
						|
                    angles[i] = UP
 | 
						|
            elif orient == 0:
 | 
						|
                if is_input is not None:
 | 
						|
                    angles[i] = RIGHT
 | 
						|
            else:
 | 
						|
                if orient != -1:
 | 
						|
                    raise ValueError(
 | 
						|
                        f"The value of orientations[{i}] is {orient}, "
 | 
						|
                        f"but it must be -1, 0, or 1")
 | 
						|
                if is_input:
 | 
						|
                    angles[i] = UP
 | 
						|
                elif is_input is False:
 | 
						|
                    angles[i] = DOWN
 | 
						|
 | 
						|
        # Justify the lengths of the paths.
 | 
						|
        if np.iterable(pathlengths):
 | 
						|
            if len(pathlengths) != n:
 | 
						|
                raise ValueError(
 | 
						|
                    f"The lengths of 'flows' ({n}) and 'pathlengths' "
 | 
						|
                    f"({len(pathlengths)}) are incompatible")
 | 
						|
        else:  # Make pathlengths into a list.
 | 
						|
            urlength = pathlengths
 | 
						|
            ullength = pathlengths
 | 
						|
            lrlength = pathlengths
 | 
						|
            lllength = pathlengths
 | 
						|
            d = dict(RIGHT=pathlengths)
 | 
						|
            pathlengths = [d.get(angle, 0) for angle in angles]
 | 
						|
            # Determine the lengths of the top-side arrows
 | 
						|
            # from the middle outwards.
 | 
						|
            for i, (angle, is_input, flow) in enumerate(zip(angles, are_inputs,
 | 
						|
                                                            scaled_flows)):
 | 
						|
                if angle == DOWN and is_input:
 | 
						|
                    pathlengths[i] = ullength
 | 
						|
                    ullength += flow
 | 
						|
                elif angle == UP and is_input is False:
 | 
						|
                    pathlengths[i] = urlength
 | 
						|
                    urlength -= flow  # Flow is negative for outputs.
 | 
						|
            # Determine the lengths of the bottom-side arrows
 | 
						|
            # from the middle outwards.
 | 
						|
            for i, (angle, is_input, flow) in enumerate(reversed(list(zip(
 | 
						|
                  angles, are_inputs, scaled_flows)))):
 | 
						|
                if angle == UP and is_input:
 | 
						|
                    pathlengths[n - i - 1] = lllength
 | 
						|
                    lllength += flow
 | 
						|
                elif angle == DOWN and is_input is False:
 | 
						|
                    pathlengths[n - i - 1] = lrlength
 | 
						|
                    lrlength -= flow
 | 
						|
            # Determine the lengths of the left-side arrows
 | 
						|
            # from the bottom upwards.
 | 
						|
            has_left_input = False
 | 
						|
            for i, (angle, is_input, spec) in enumerate(reversed(list(zip(
 | 
						|
                  angles, are_inputs, zip(scaled_flows, pathlengths))))):
 | 
						|
                if angle == RIGHT:
 | 
						|
                    if is_input:
 | 
						|
                        if has_left_input:
 | 
						|
                            pathlengths[n - i - 1] = 0
 | 
						|
                        else:
 | 
						|
                            has_left_input = True
 | 
						|
            # Determine the lengths of the right-side arrows
 | 
						|
            # from the top downwards.
 | 
						|
            has_right_output = False
 | 
						|
            for i, (angle, is_input, spec) in enumerate(zip(
 | 
						|
                  angles, are_inputs, list(zip(scaled_flows, pathlengths)))):
 | 
						|
                if angle == RIGHT:
 | 
						|
                    if is_input is False:
 | 
						|
                        if has_right_output:
 | 
						|
                            pathlengths[i] = 0
 | 
						|
                        else:
 | 
						|
                            has_right_output = True
 | 
						|
 | 
						|
        # Begin the subpaths, and smooth the transition if the sum of the flows
 | 
						|
        # is nonzero.
 | 
						|
        urpath = [(Path.MOVETO, [(self.gap - trunklength / 2.0),  # Upper right
 | 
						|
                                 gain / 2.0]),
 | 
						|
                  (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0,
 | 
						|
                                 gain / 2.0]),
 | 
						|
                  (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0,
 | 
						|
                                 gain / 2.0]),
 | 
						|
                  (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0,
 | 
						|
                                 -loss / 2.0]),
 | 
						|
                  (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0,
 | 
						|
                                 -loss / 2.0]),
 | 
						|
                  (Path.LINETO, [(trunklength / 2.0 - self.gap),
 | 
						|
                                 -loss / 2.0])]
 | 
						|
        llpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap),  # Lower left
 | 
						|
                                 loss / 2.0]),
 | 
						|
                  (Path.LINETO, [(trunklength / 2.0 - self.gap) / 2.0,
 | 
						|
                                 loss / 2.0]),
 | 
						|
                  (Path.CURVE4, [(trunklength / 2.0 - self.gap) / 8.0,
 | 
						|
                                 loss / 2.0]),
 | 
						|
                  (Path.CURVE4, [(self.gap - trunklength / 2.0) / 8.0,
 | 
						|
                                 -gain / 2.0]),
 | 
						|
                  (Path.LINETO, [(self.gap - trunklength / 2.0) / 2.0,
 | 
						|
                                 -gain / 2.0]),
 | 
						|
                  (Path.LINETO, [(self.gap - trunklength / 2.0),
 | 
						|
                                 -gain / 2.0])]
 | 
						|
        lrpath = [(Path.LINETO, [(trunklength / 2.0 - self.gap),  # Lower right
 | 
						|
                                 loss / 2.0])]
 | 
						|
        ulpath = [(Path.LINETO, [self.gap - trunklength / 2.0,  # Upper left
 | 
						|
                                 gain / 2.0])]
 | 
						|
 | 
						|
        # Add the subpaths and assign the locations of the tips and labels.
 | 
						|
        tips = np.zeros((n, 2))
 | 
						|
        label_locations = np.zeros((n, 2))
 | 
						|
        # Add the top-side inputs and outputs from the middle outwards.
 | 
						|
        for i, (angle, is_input, spec) in enumerate(zip(
 | 
						|
              angles, are_inputs, list(zip(scaled_flows, pathlengths)))):
 | 
						|
            if angle == DOWN and is_input:
 | 
						|
                tips[i, :], label_locations[i, :] = self._add_input(
 | 
						|
                    ulpath, angle, *spec)
 | 
						|
            elif angle == UP and is_input is False:
 | 
						|
                tips[i, :], label_locations[i, :] = self._add_output(
 | 
						|
                    urpath, angle, *spec)
 | 
						|
        # Add the bottom-side inputs and outputs from the middle outwards.
 | 
						|
        for i, (angle, is_input, spec) in enumerate(reversed(list(zip(
 | 
						|
              angles, are_inputs, list(zip(scaled_flows, pathlengths)))))):
 | 
						|
            if angle == UP and is_input:
 | 
						|
                tip, label_location = self._add_input(llpath, angle, *spec)
 | 
						|
                tips[n - i - 1, :] = tip
 | 
						|
                label_locations[n - i - 1, :] = label_location
 | 
						|
            elif angle == DOWN and is_input is False:
 | 
						|
                tip, label_location = self._add_output(lrpath, angle, *spec)
 | 
						|
                tips[n - i - 1, :] = tip
 | 
						|
                label_locations[n - i - 1, :] = label_location
 | 
						|
        # Add the left-side inputs from the bottom upwards.
 | 
						|
        has_left_input = False
 | 
						|
        for i, (angle, is_input, spec) in enumerate(reversed(list(zip(
 | 
						|
              angles, are_inputs, list(zip(scaled_flows, pathlengths)))))):
 | 
						|
            if angle == RIGHT and is_input:
 | 
						|
                if not has_left_input:
 | 
						|
                    # Make sure the lower path extends
 | 
						|
                    # at least as far as the upper one.
 | 
						|
                    if llpath[-1][1][0] > ulpath[-1][1][0]:
 | 
						|
                        llpath.append((Path.LINETO, [ulpath[-1][1][0],
 | 
						|
                                                     llpath[-1][1][1]]))
 | 
						|
                    has_left_input = True
 | 
						|
                tip, label_location = self._add_input(llpath, angle, *spec)
 | 
						|
                tips[n - i - 1, :] = tip
 | 
						|
                label_locations[n - i - 1, :] = label_location
 | 
						|
        # Add the right-side outputs from the top downwards.
 | 
						|
        has_right_output = False
 | 
						|
        for i, (angle, is_input, spec) in enumerate(zip(
 | 
						|
              angles, are_inputs, list(zip(scaled_flows, pathlengths)))):
 | 
						|
            if angle == RIGHT and is_input is False:
 | 
						|
                if not has_right_output:
 | 
						|
                    # Make sure the upper path extends
 | 
						|
                    # at least as far as the lower one.
 | 
						|
                    if urpath[-1][1][0] < lrpath[-1][1][0]:
 | 
						|
                        urpath.append((Path.LINETO, [lrpath[-1][1][0],
 | 
						|
                                                     urpath[-1][1][1]]))
 | 
						|
                    has_right_output = True
 | 
						|
                tips[i, :], label_locations[i, :] = self._add_output(
 | 
						|
                    urpath, angle, *spec)
 | 
						|
        # Trim any hanging vertices.
 | 
						|
        if not has_left_input:
 | 
						|
            ulpath.pop()
 | 
						|
            llpath.pop()
 | 
						|
        if not has_right_output:
 | 
						|
            lrpath.pop()
 | 
						|
            urpath.pop()
 | 
						|
 | 
						|
        # Concatenate the subpaths in the correct order (clockwise from top).
 | 
						|
        path = (urpath + self._revert(lrpath) + llpath + self._revert(ulpath) +
 | 
						|
                [(Path.CLOSEPOLY, urpath[0][1])])
 | 
						|
 | 
						|
        # Create a patch with the Sankey outline.
 | 
						|
        codes, vertices = zip(*path)
 | 
						|
        vertices = np.array(vertices)
 | 
						|
 | 
						|
        def _get_angle(a, r):
 | 
						|
            if a is None:
 | 
						|
                return None
 | 
						|
            else:
 | 
						|
                return a + r
 | 
						|
 | 
						|
        if prior is None:
 | 
						|
            if rotation != 0:  # By default, none of this is needed.
 | 
						|
                angles = [_get_angle(angle, rotation) for angle in angles]
 | 
						|
                rotate = Affine2D().rotate_deg(rotation * 90).transform_affine
 | 
						|
                tips = rotate(tips)
 | 
						|
                label_locations = rotate(label_locations)
 | 
						|
                vertices = rotate(vertices)
 | 
						|
            text = self.ax.text(0, 0, s=patchlabel, ha='center', va='center')
 | 
						|
        else:
 | 
						|
            rotation = (self.diagrams[prior].angles[connect[0]] -
 | 
						|
                        angles[connect[1]])
 | 
						|
            angles = [_get_angle(angle, rotation) for angle in angles]
 | 
						|
            rotate = Affine2D().rotate_deg(rotation * 90).transform_affine
 | 
						|
            tips = rotate(tips)
 | 
						|
            offset = self.diagrams[prior].tips[connect[0]] - tips[connect[1]]
 | 
						|
            translate = Affine2D().translate(*offset).transform_affine
 | 
						|
            tips = translate(tips)
 | 
						|
            label_locations = translate(rotate(label_locations))
 | 
						|
            vertices = translate(rotate(vertices))
 | 
						|
            kwds = dict(s=patchlabel, ha='center', va='center')
 | 
						|
            text = self.ax.text(*offset, **kwds)
 | 
						|
        if mpl.rcParams['_internal.classic_mode']:
 | 
						|
            fc = kwargs.pop('fc', kwargs.pop('facecolor', '#bfd1d4'))
 | 
						|
            lw = kwargs.pop('lw', kwargs.pop('linewidth', 0.5))
 | 
						|
        else:
 | 
						|
            fc = kwargs.pop('fc', kwargs.pop('facecolor', None))
 | 
						|
            lw = kwargs.pop('lw', kwargs.pop('linewidth', None))
 | 
						|
        if fc is None:
 | 
						|
            fc = self.ax._get_patches_for_fill.get_next_color()
 | 
						|
        patch = PathPatch(Path(vertices, codes), fc=fc, lw=lw, **kwargs)
 | 
						|
        self.ax.add_patch(patch)
 | 
						|
 | 
						|
        # Add the path labels.
 | 
						|
        texts = []
 | 
						|
        for number, angle, label, location in zip(flows, angles, labels,
 | 
						|
                                                  label_locations):
 | 
						|
            if label is None or angle is None:
 | 
						|
                label = ''
 | 
						|
            elif self.unit is not None:
 | 
						|
                if isinstance(self.format, str):
 | 
						|
                    quantity = self.format % abs(number) + self.unit
 | 
						|
                elif callable(self.format):
 | 
						|
                    quantity = self.format(number)
 | 
						|
                else:
 | 
						|
                    raise TypeError(
 | 
						|
                        'format must be callable or a format string')
 | 
						|
                if label != '':
 | 
						|
                    label += "\n"
 | 
						|
                label += quantity
 | 
						|
            texts.append(self.ax.text(x=location[0], y=location[1],
 | 
						|
                                      s=label,
 | 
						|
                                      ha='center', va='center'))
 | 
						|
        # Text objects are placed even they are empty (as long as the magnitude
 | 
						|
        # of the corresponding flow is larger than the tolerance) in case the
 | 
						|
        # user wants to provide labels later.
 | 
						|
 | 
						|
        # Expand the size of the diagram if necessary.
 | 
						|
        self.extent = (min(np.min(vertices[:, 0]),
 | 
						|
                           np.min(label_locations[:, 0]),
 | 
						|
                           self.extent[0]),
 | 
						|
                       max(np.max(vertices[:, 0]),
 | 
						|
                           np.max(label_locations[:, 0]),
 | 
						|
                           self.extent[1]),
 | 
						|
                       min(np.min(vertices[:, 1]),
 | 
						|
                           np.min(label_locations[:, 1]),
 | 
						|
                           self.extent[2]),
 | 
						|
                       max(np.max(vertices[:, 1]),
 | 
						|
                           np.max(label_locations[:, 1]),
 | 
						|
                           self.extent[3]))
 | 
						|
        # Include both vertices _and_ label locations in the extents; there are
 | 
						|
        # where either could determine the margins (e.g., arrow shoulders).
 | 
						|
 | 
						|
        # Add this diagram as a subdiagram.
 | 
						|
        self.diagrams.append(
 | 
						|
            SimpleNamespace(patch=patch, flows=flows, angles=angles, tips=tips,
 | 
						|
                            text=text, texts=texts))
 | 
						|
 | 
						|
        # Allow a daisy-chained call structure (see docstring for the class).
 | 
						|
        return self
 | 
						|
 | 
						|
    def finish(self):
 | 
						|
        """
 | 
						|
        Adjust the Axes and return a list of information about the Sankey
 | 
						|
        subdiagram(s).
 | 
						|
 | 
						|
        Returns a list of subdiagrams with the following fields:
 | 
						|
 | 
						|
        ========  =============================================================
 | 
						|
        Field     Description
 | 
						|
        ========  =============================================================
 | 
						|
        *patch*   Sankey outline (a `~matplotlib.patches.PathPatch`).
 | 
						|
        *flows*   Flow values (positive for input, negative for output).
 | 
						|
        *angles*  List of angles of the arrows [deg/90].
 | 
						|
                  For example, if the diagram has not been rotated,
 | 
						|
                  an input to the top side has an angle of 3 (DOWN),
 | 
						|
                  and an output from the top side has an angle of 1 (UP).
 | 
						|
                  If a flow has been skipped (because its magnitude is less
 | 
						|
                  than *tolerance*), then its angle will be *None*.
 | 
						|
        *tips*    (N, 2)-array of the (x, y) positions of the tips (or "dips")
 | 
						|
                  of the flow paths.
 | 
						|
                  If the magnitude of a flow is less the *tolerance* of this
 | 
						|
                  `Sankey` instance, the flow is skipped and its tip will be at
 | 
						|
                  the center of the diagram.
 | 
						|
        *text*    `.Text` instance for the diagram label.
 | 
						|
        *texts*   List of `.Text` instances for the flow labels.
 | 
						|
        ========  =============================================================
 | 
						|
 | 
						|
        See Also
 | 
						|
        --------
 | 
						|
        Sankey.add
 | 
						|
        """
 | 
						|
        self.ax.axis([self.extent[0] - self.margin,
 | 
						|
                      self.extent[1] + self.margin,
 | 
						|
                      self.extent[2] - self.margin,
 | 
						|
                      self.extent[3] + self.margin])
 | 
						|
        self.ax.set_aspect('equal', adjustable='datalim')
 | 
						|
        return self.diagrams
 |