docs: def sankey(*args, projection=None,
             start=None, end=None, path=None,
             hue=None, categorical=False, scheme=None, k=5, cmap='viridis', vmin=None, vmax=None,
             legend=False, legend_kwargs=None, legend_labels=None, legend_values=None, legend_var=None,
             extent=None, figsize=(8, 6), ax=None,
             scale=None, limits=(1, 5), scale_func=None,
             **kwargs):
      """
      Spatial Sankey or flow map.
  
      Parameters
      ----------
      df : GeoDataFrame, optional.
          The data being plotted. This parameter is optional - it is not needed if ``start`` and ``end`` (and ``hue``,
          if provided) are iterables.
      projection : geoplot.crs object instance, optional
          A geographic projection. For more information refer to `the tutorial page on projections
          <https://nbviewer.jupyter.org/github/ResidentMario/geoplot/blob/master/notebooks/tutorials/Projections.ipynb>`_.
      start : str or iterable
          A list of starting points. This parameter is required.
      end : str or iterable
          A list of ending points. This parameter is required.
      path : geoplot.crs object instance or iterable, optional
          Pass an iterable of paths to draw custom paths (see `this example
          <https://residentmario.github.io/geoplot/examples/dc-street-network.html>`_), or a projection to draw
          the shortest paths in that given projection. The default is ``Geodetic()``, which will connect points using
          `great circle distance <https://en.wikipedia.org/wiki/Great-circle_distance>`_—the true shortest
          path on the surface of the Earth.
      hue : None, Series, GeoSeries, iterable, or str, optional
          Applies a colormap to the output points.
      categorical : boolean, optional
          Set to ``True`` if ``hue`` references a categorical variable, and ``False`` (the default) otherwise. Ignored
          if ``hue`` is left unspecified.
      scheme : None or {"quantiles"|"equal_interval"|"fisher_jenks"}, optional
          Controls how the colormap bin edges are determined. Ignored if ``hue`` is left unspecified.
      k : int or None, optional
          Ignored if ``hue`` is left unspecified. Otherwise, if ``categorical`` is False, controls how many colors to
          use (5 is the default). If set to ``None``, a continuous colormap will be used.
      cmap : matplotlib color, optional
          The `matplotlib colormap <http://matplotlib.org/examples/color/colormaps_reference.html>`_ to be used.
          Ignored if ``hue`` is left unspecified.
      vmin : float, optional
          Values below this level will be colored the same threshold value. Defaults to the dataset minimum. Ignored
          if ``hue`` is left unspecified.
      vmax : float, optional
          Values above this level will be colored the same threshold value. Defaults to the dataset maximum. Ignored
          if ``hue`` is left unspecified.
      scale : str or iterable, optional
          Applies scaling to the output points. Defaults to None (no scaling).
      limits : (min, max) tuple, optional
          The minimum and maximum scale limits. Ignored if ``scale`` is left specified.
      scale_func : ufunc, optional
          The function used to scale point sizes. Defaults to a linear scale. For more information see `the Gallery demo
          <examples/usa-city-elevations.html>`_.
      legend : boolean, optional
          Whether or not to include a legend. Ignored if neither a ``hue`` nor a ``scale`` is specified.
      legend_values : list, optional
          The values to use in the legend. Defaults to equal intervals. For more information see `the Gallery demo
          <https://residentmario.github.io/geoplot/examples/largest-cities-usa.html>`_.
      legend_labels : list, optional
          The names to use in the legend. Defaults to the variable values. For more information see `the Gallery demo
          <https://residentmario.github.io/geoplot/examples/largest-cities-usa.html>`_.
      legend_var : "hue" or "scale", optional
          If both ``hue`` and ``scale`` are specified, which variable to use in the legend.
      legend_kwargs : dict, optional
          Keyword arguments to be passed to `the underlying legend <http://matplotlib.org/users/legend_guide.html>`_.
      extent : None or (minx, maxx, miny, maxy), optional
          Used to control plot x-axis and y-axis limits manually.
      figsize : tuple, optional
          An (x, y) tuple passed to ``matplotlib.figure`` which sets the size, in inches, of the resultant plot.
      ax : AxesSubplot or GeoAxesSubplot instance, optional
          A ``matplotlib.axes.AxesSubplot`` or ``cartopy.mpl.geoaxes.GeoAxesSubplot`` instance. Defaults to a new axis.
      kwargs: dict, optional
          Keyword arguments to be passed to the underlying ``matplotlib`` `Line2D
          <https://matplotlib.org/api/_as_gen/matplotlib.lines.Line2D.html#matplotlib.lines.Line2D>`_ instances.
  
      Returns
      -------
      ``AxesSubplot`` or ``GeoAxesSubplot``
          The plot axis
  
      Examples
      --------
      A `Sankey diagram <https://en.wikipedia.org/wiki/Sankey_diagram>`_ is a simple visualization demonstrating flow
      through a network. A Sankey diagram is useful when you wish to show the volume of things moving between points or
      spaces: traffic load a road network, for example, or inter-airport travel volumes. The ``geoplot`` ``sankey``
      adds spatial context to this plot type by laying out the points in meaningful locations: airport locations, say,
      or road intersections.
  
      A basic ``sankey`` specifies data, ``start`` points, ``end`` points, and, optionally, a projection. The ``df``
      argument is optional; if geometries are provided as independent iterables it is ignored. We overlay world
      geometry to aid interpretability.
  
      .. code-block:: python
  
          ax = gplt.sankey(la_flights, start='start', end='end', projection=gcrs.PlateCarree())
          ax.set_global(); ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-geospatial-context.png
  
      The lines appear curved because they are `great circle <https://en.wikipedia.org/wiki/Great-circle_distance>`_
      paths, which are the shortest routes between points on a sphere.
  
      .. code-block:: python
  
          ax = gplt.sankey(la_flights, start='start', end='end', projection=gcrs.Orthographic())
          ax.set_global(); ax.coastlines(); ax.outline_patch.set_visible(True)
  
      .. image:: ../figures/sankey/sankey-greatest-circle-distance.png
  
      To plot using a different distance metric pass a ``cartopy`` ``crs`` object (*not* a ``geoplot`` one) to the
      ``path`` parameter.
  
      .. code-block:: python
  
          import cartopy.crs as ccrs
          ax = gplt.sankey(la_flights, start='start', end='end', projection=gcrs.PlateCarree(), path=ccrs.PlateCarree())
          ax.set_global(); ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-path-projection.png
  
      If your data has custom paths, you can use those instead, via the ``path`` parameter.
  
      .. code-block:: python
  
          gplt.sankey(dc, path=dc.geometry, projection=gcrs.AlbersEqualArea(), scale='aadt')
  
  
      .. image:: ../figures/sankey/sankey-path.png
  
      ``hue`` parameterizes the color, and ``cmap`` controls the colormap. ``legend`` adds a a legend. Keyword
      arguments can be passed to the legend using the ``legend_kwargs`` argument. These arguments will be
      passed to the underlying ``matplotlib`` `Legend
      <http://matplotlib.org/api/legend_api.html#matplotlib.legend.Legend>`_. The ``loc`` and ``bbox_to_anchor``
      parameters are particularly useful for positioning the legend.
  
      .. code-block:: python
  
          ax = gplt.sankey(network, projection=gcrs.PlateCarree(),
                           start='from', end='to',
                           hue='mock_variable', cmap='RdYlBu',
                           legend=True, legend_kwargs={'bbox_to_anchor': (1.4, 1.0)})
          ax.set_global()
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-legend-kwargs.png
  
      Change the number of bins by specifying an alternative ``k`` value. To use a continuous colormap, explicitly
      specify ``k=None``. You can change the binning sceme with ``scheme``. The default is ``quantile``, which bins
      observations into classes of different sizes but the same numbers of observations. ``equal_interval`` will
      creates bins that are the same size, but potentially containing different numbers of observations. The more
      complicated ``fisher_jenks`` scheme is an intermediate between the two.
  
      .. code-block:: python
  
          ax = gplt.sankey(network, projection=gcrs.PlateCarree(),
                           start='from', end='to',
                           hue='mock_variable', cmap='RdYlBu',
                           legend=True, legend_kwargs={'bbox_to_anchor': (1.25, 1.0)},
                           k=3, scheme='equal_interval')
          ax.set_global()
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-scheme.png
  
      If your variable of interest is already `categorical
      <http://pandas.pydata.org/pandas-docs/stable/categorical.html>`_, specify ``categorical=True`` to
      use the labels in your dataset directly.
  
      .. code-block:: python
  
          ax = gplt.sankey(network, projection=gcrs.PlateCarree(),
                           start='from', end='to',
                           hue='above_meridian', cmap='RdYlBu',
                           legend=True, legend_kwargs={'bbox_to_anchor': (1.2, 1.0)},
                           categorical=True)
          ax.set_global()
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-categorical.png
  
      ``scale`` can be used to enable ``linewidth`` as a visual variable. Adjust the upper and lower bound with the
      ``limits`` parameter.
  
      .. code-block:: python
  
          ax = gplt.sankey(la_flights, projection=gcrs.PlateCarree(),
                           extent=(-125.0011, -66.9326, 24.9493, 49.5904),
                           start='start', end='end',
                           scale='Passengers',
                           limits=(0.1, 5),
                           legend=True, legend_kwargs={'bbox_to_anchor': (1.1, 1.0)})
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-scale.png
  
      The default scaling function is linear: an observations at the midpoint of two others will be exactly midway
      between them in size. To specify an alternative scaling function, use the ``scale_func`` parameter. This should
      be a factory function of two variables which, when given the maximum and minimum of the dataset,
      returns a scaling function which will be applied to the rest of the data. A demo is available in
      the `example gallery <examples/usa-city-elevations.html>`_.
  
      .. code-block:: python
  
          def trivial_scale(minval, maxval): return lambda v: 1
          ax = gplt.sankey(la_flights, projection=gcrs.PlateCarree(),
                           extent=(-125.0011, -66.9326, 24.9493, 49.5904),
                           start='start', end='end',
                           scale='Passengers', scale_func=trivial_scale,
                           legend=True, legend_kwargs={'bbox_to_anchor': (1.1, 1.0)})
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-scale-func.png
  
      ``hue`` and ``scale`` can co-exist. In case more than one visual variable is used, control which one appears in
      the legend using ``legend_var``.
  
      .. code-block:: python
  
          ax = gplt.sankey(network, projection=gcrs.PlateCarree(),
                   start='from', end='to',
                   scale='mock_data',
                   legend=True, legend_kwargs={'bbox_to_anchor': (1.1, 1.0)},
                   hue='mock_data', legend_var="hue")
          ax.set_global()
          ax.coastlines()
  
      .. image:: ../figures/sankey/sankey-legend-var.png
  
      """
      # Validate df.
      if len(args) > 1:
          raise ValueError("Invalid input.")
      elif len(args) == 1:
          df = args[0]
      else:
          df = pd.DataFrame()
          # df = None  # bind the local name here; initialize in a bit.
  
      # Validate the rest of the input.
      if ((start is None) or (end is None)) and not hasattr(path, "__iter__"):
          raise ValueError("The 'start' and 'end' parameters must both be specified.")
      if (isinstance(start, str) or isinstance(end, str)) and df.empty:
          raise ValueError("Invalid input.")
      if isinstance(start, str):
          start = df[start]
      elif start is not None:
          start = gpd.GeoSeries(start)
      if isinstance(end, str):
          end = df[end]
      elif end is not None:
          end = gpd.GeoSeries(end)
      if (start is not None) and (end is not None) and hasattr(path, "__iter__"):
          raise ValueError("One of 'start' and 'end' OR 'path' must be specified, but they cannot be specified "
                           "simultaneously.")
      if path is None:  # No path provided.
          path = ccrs.Geodetic()
          path_geoms = None
      elif isinstance(path, str):  # Path is a column in the dataset.
          path_geoms = df[path]
      elif hasattr(path, "__iter__"):  # Path is an iterable.
          path_geoms = gpd.GeoSeries(path)
      else:  # Path is a cartopy.crs object.
          path_geoms = None
      if start is not None and end is not None:
          points = pd.concat([start, end])
      else:
          points = None
  
      # Set legend variable.
      if legend_var is None:
          if scale is not None:
              legend_var = "scale"
          elif hue is not None:
              legend_var = "hue"
  
      # After validating the inputs, we are in one of two modes:
      # 1. Projective mode. In this case ``path_geoms`` is None, while ``points`` contains a concatenation of our
      #    points (for use in initializing the plot extents). This case occurs when the user specifies ``start`` and
      #    ``end``, and not ``path``. This is "projective mode" because it means that ``path`` will be a
      #    projection---if one is not provided explicitly, the ``gcrs.Geodetic()`` projection.
      # 2. Path mode. In this case ``path_geoms`` is an iterable of LineString entities to be plotted, while ``points``
      #    is None. This occurs when the user specifies ``path``, and not ``start`` or ``end``. This is path mode
      #    because we will need to plot exactly those paths!
  
      # At this point we'll initialize the rest of the variables we need. The way that we initialize them is going to
      # depend on which code path we are on. Additionally, we will initialize the `df` variable with a projection
      # dummy, if it has not been initialized already. This `df` will only be used for figuring out the extent,
      # and will be discarded afterwards!
      #
      # Variables we need to generate at this point, and why we need them:
      # 1. (clong, clat) --- To pass this to the projection settings.
      # 2. (xmin. xmax, ymin. ymax) --- To pass this to the extent settings.
      # 3. n --- To pass this to the color array in case no ``color`` is specified.
      if path_geoms is None and points is not None:
          if df.empty:
              df = gpd.GeoDataFrame(geometry=points)
          xs = np.array([p.x for p in points])
          ys = np.array([p.y for p in points])
          xmin, xmax, ymin, ymax = np.min(xs), np.max(xs), np.min(ys), np.max(ys)
          clong, clat = np.mean(xs), np.mean(ys)
          n = int(len(points) / 2)
      else:  # path_geoms is an iterable
          path_geoms = gpd.GeoSeries(path_geoms)
          xmin, xmax, ymin, ymax = _get_envelopes_min_maxes(path_geoms.envelope.exterior)
          clong, clat = (xmin + xmax) / 2, (ymin + ymax) / 2
          n = len(path_geoms)
  
      # Initialize the figure.
      fig = _init_figure(ax, figsize)
  
      # Load the projection.
      if projection:
          projection = projection.load(df, {
              'central_longitude': lambda df: clong,
              'central_latitude': lambda df: clat
          })
  
          # Set up the axis.
          if not ax:
              ax = plt.subplot(111, projection=projection)
      else:
          if not ax:
              ax = plt.gca()
  
      # Clean up patches.
      _lay_out_axes(ax, projection)
  
      # Set extent.
      if projection:
          if extent:
              ax.set_extent(extent)
          else:
              ax.set_extent((xmin, xmax, ymin, ymax))
      else:
          if extent:
              ax.set_xlim((extent[0], extent[1]))
              ax.set_ylim((extent[2], extent[3]))
          else:
              ax.set_xlim((xmin, xmax))
              ax.set_ylim((ymin, ymax))
  
      # Generate the coloring information, if needed. Follows one of two schemes, categorical or continuous,
      # based on whether or not ``k`` is specified (``hue`` must be specified for either to work).
      if k is not None:
          # Categorical colormap code path.
          categorical, k, scheme = _validate_buckets(categorical, k, scheme)
  
          hue = _validate_hue(df, hue)
  
          if hue is not None:
              cmap, categories, hue_values = _discrete_colorize(categorical, hue, scheme, k, cmap, vmin, vmax)
              colors = [cmap.to_rgba(v) for v in hue_values]
  
              # Add a legend, if appropriate.
              if legend and (legend_var != "scale" or scale is None):
                  _paint_hue_legend(ax, categories, cmap, legend_labels, legend_kwargs)
          else:
              if 'color' not in kwargs.keys():
                  colors = ['steelblue'] * n
              else:
                  colors = [kwargs['color']] * n
                  kwargs.pop('color')
      elif k is None and hue is not None:
          # Continuous colormap code path.
          hue_values = hue
          cmap = _continuous_colormap(hue_values, cmap, vmin, vmax)
          colors = [cmap.to_rgba(v) for v in hue_values]
  
          # Add a legend, if appropriate.
          if legend and (legend_var != "scale" or scale is None):
              _paint_colorbar_legend(ax, hue_values, cmap, legend_kwargs)
  
      # Check if the ``scale`` parameter is filled, and use it to fill a ``values`` name.
      if scale:
          if isinstance(scale, str):
              scalar_values = df[scale]
          else:
              scalar_values = scale
  
          # Compute a scale function.
          dmin, dmax = np.min(scalar_values), np.max(scalar_values)
          if not scale_func:
              dslope = (limits[1] - limits[0]) / (dmax - dmin)
              dscale = lambda dval: limits[0] + dslope * (dval - dmin)
          else:
              dscale = scale_func(dmin, dmax)
  
          # Apply the scale function.
          scalar_multiples = np.array([dscale(d) for d in scalar_values])
          widths = scalar_multiples * 1
  
          # Draw a legend, if appropriate.
          if legend and (legend_var == "scale"):
              _paint_carto_legend(ax, scalar_values, legend_values, legend_labels, dscale, legend_kwargs)
      else:
          widths = [1] * n  # pyplot default
  
      # Allow overwriting visual arguments.
      if 'linestyle' in kwargs.keys():
          linestyle = kwargs['linestyle']; kwargs.pop('linestyle')
      else:
          linestyle = '-'
      if 'color' in kwargs.keys():
          colors = [kwargs['color']]*n; kwargs.pop('color')
      elif 'edgecolor' in kwargs.keys():  # plt.plot uses 'color', mpl.ax.add_feature uses 'edgecolor'. Support both.
          colors = [kwargs['edgecolor']]*n; kwargs.pop('edgecolor')
      if 'linewidth' in kwargs.keys():
          widths = [kwargs['linewidth']]*n; kwargs.pop('linewidth')
  
      if projection:
          # Duck test plot. The first will work if a valid transformation is passed to ``path`` (e.g. we are in the
          # ``start + ``end`` case), the second will work if ``path`` is an iterable (e.g. we are in the ``path`` case).
          try:
              for origin, destination, color, width in zip(start, end, colors, widths):
                  ax.plot([origin.x, destination.x], [origin.y, destination.y], transform=path,
                          linestyle=linestyle, linewidth=width, color=color, **kwargs)
          except TypeError:
              for line, color, width in zip(path_geoms, colors, widths):
                  feature = ShapelyFeature([line], ccrs.PlateCarree())
                  ax.add_feature(feature, linestyle=linestyle, linewidth=width, edgecolor=color, facecolor='None',
                  **kwargs)
      else:
          try:
              for origin, destination, color, width in zip(start, end, colors, widths):
                  ax.plot([origin.x, destination.x], [origin.y, destination.y],
                          linestyle=linestyle, linewidth=width, color=color, **kwargs)
          except TypeError:
              for path, color, width in zip(path_geoms, colors, widths):
                  # We have to implement different methods for dealing with LineString and MultiLineString objects.
                  # This calls for, yep, another duck test.
                  try:  # LineString
                      line = mpl.lines.Line2D([coord[0] for coord in path.coords],
                                              [coord[1] for coord in path.coords],
                                              linestyle=linestyle, linewidth=width, color=color, **kwargs)
                      ax.add_line(line)
                  except NotImplementedError:  # MultiLineString
                      for line in path:
                          line = mpl.lines.Line2D([coord[0] for coord in line.coords],
                                                  [coord[1] for coord in line.coords],
                                                  linestyle=linestyle, linewidth=width, color=color, **kwargs)
                          ax.add_line(line)
      return ax