def pointplot(df, projection=None,
              hue=None, categorical=False, scheme=None, k=5, cmap='Set1', vmin=None, vmax=None,
              scale=None, limits=(0.5, 2), scale_func=None,
              legend=False, legend_values=None, legend_labels=None, legend_kwargs=None, legend_var=None,
              figsize=(8, 6), extent=None, ax=None, **kwargs):
    """
    Geospatial scatter plot.

    Parameters
    ----------
    df : GeoDataFrame
        The data being plotted.
    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>`_.
    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 `scatter plot
        <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.scatter>`_.

    Returns
    -------
    ``AxesSubplot`` or ``GeoAxesSubplot``
        The plot axis

    Examples
    --------

    The ``pointplot`` is a `geospatial scatter plot <https://en.wikipedia.org/wiki/Scatter_plot>`_ representing
    each observation in your dataset with a single point. It is simple and easily interpretable plot that is nearly
    universally understood, making it an ideal choice for showing simple pointwise relationships between
    observations.

    The expected input is a ``GeoDataFrame`` containing geometries of the ``shapely.geometry.Point`` type. A
    bare-bones pointplot goes thusly:

    .. code-block:: python

        import geoplot as gplt
        import geoplot.crs as gcrs
        gplt.pointplot(points)

    .. image:: ../figures/pointplot/pointplot-initial.png


    The ``hue`` parameter accepts a data column and applies a colormap to the output. The ``legend`` parameter
    toggles a legend.

    .. code-block:: python

        gplt.pointplot(cities, projection=gcrs.AlbersEqualArea(), hue='ELEV_IN_FT', legend=True)

    .. image:: ../figures/pointplot/pointplot-legend.png

    The ``pointplot`` binning methodology is controlled using by `scheme`` parameter. 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

        gplt.pointplot(cities, projection=gcrs.AlbersEqualArea(), hue='ELEV_IN_FT',
                       legend=True, scheme='equal_interval')

    .. image:: ../figures/pointplot/pointplot-scheme.png

    Alternatively, your data may already be `categorical
    <http://pandas.pydata.org/pandas-docs/stable/categorical.html>`_. In that case specify ``categorical=True`` instead.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(), hue='BOROUGH',
                       legend=True, categorical=True)

    .. image:: ../figures/pointplot/pointplot-categorical.png

    Keyword arguments can be passed to the legend using the ``legend_kwargs`` argument. These arguments will be
    passed to the underlying ``matplotlib.legend.Legend`` instance (`ref
    <http://matplotlib.org/api/legend_api.html#matplotlib.legend.Legend>`_). The ``loc`` and ``bbox_to_anchor``
    parameters are particularly useful for positioning the legend. Other additional arguments will be passed to the
    underlying ``matplotlib`` `scatter plot <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.scatter>`_.

    .. code-block:: python

        gplt.pointplot(collisions[collisions['BOROUGH'].notnull()], projection=gcrs.AlbersEqualArea(),
                       hue='BOROUGH', categorical=True,
                       legend=True, legend_kwargs={'loc': 'upper left'},
                       edgecolor='white', linewidth=0.5)

    .. image:: ../figures/pointplot/pointplot-kwargs.png

    Change the number of bins by specifying an alternative ``k`` value. Adjust the `colormap
    <http://matplotlib.org/examples/color/colormaps_reference.html>`_ using the ``cmap`` parameter. To use a
    continuous colormap, explicitly specify ``k=None``. Note that if ``legend=True``, a ``matplotlib``
    `colorbar legend <http://matplotlib.org/api/colorbar_api.html>`_ will be used.

    .. code-block:: python

        gplt.pointplot(data, projection=gcrs.AlbersEqualArea(),
                       hue='var', k=8,
                       edgecolor='white', linewidth=0.5,
                       legend=True, legend_kwargs={'bbox_to_anchor': (1.25, 1.0)})

    .. image:: ../figures/pointplot/pointplot-k.png

    ``scale`` provides an alternative or additional visual variable.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED',
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-scale.png

    The limits can be adjusted to fit your data using the ``limits`` parameter.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED', limits=(0, 10),
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-limits.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):
            def scalar(val):
                return 2
            return scalar

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED', scale_func=trivial_scale,
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-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

        gplt.pointplot(collisions[collisions['BOROUGH'].notnull()],
                       projection=gcrs.AlbersEqualArea(),
                       hue='BOROUGH', categorical=True,
                       scale='NUMBER OF PERSONS INJURED', limits=(0, 10),
                       legend=True, legend_kwargs={'loc': 'upper left'},
                       legend_var='scale')

    .. image:: ../figures/pointplot/pointplot-legend-var.png

    """
    # Initialize the figure, if one hasn't been initialized already.
    fig = _init_figure(ax, figsize)

    xs = np.array([p.x for p in df.geometry])
    ys = np.array([p.y for p in df.geometry])

    if projection:
        # Properly set up the projection.
        projection = projection.load(df, {
            'central_longitude': lambda df: np.mean(np.array([p.x for p in df.geometry.centroid])),
            'central_latitude': lambda df: np.mean(np.array([p.y for p in df.geometry.centroid]))
        })

        # Set up the axis.
        if not ax:
            ax = plt.subplot(111, projection=projection)

        # Set extent.
        if extent:
            ax.set_extent(extent)
        else:
            pass  # Default extent.
    else:
        if not ax:
            ax = plt.gca()

    # Clean up patches.
    _lay_out_axes(ax, projection)

    # Immediately return if input geometry is empty.
    if len(df.geometry) == 0:
        return ax

    # Validate hue input.
    hue = _validate_hue(df, hue)

    # Set legend variable.
    if legend_var is None:
        if hue is not None:
            legend_var = "hue"
        elif scale is not None:
            legend_var = "scale"

    # 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)

        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']*len(df)
            else:
                colors = [kwargs['color']]*len(df)
                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)
            if np.isinf(dslope):  # Edge case: if dmax, dmin are <=10**-30 or so, will overflow and eval to infinity.
                raise ValueError("The data range provided to the 'scale' variable is too small for the default "
                                 "scaling function. Normalize your data or provide a custom 'scale_func'.")
            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])
        sizes = scalar_multiples * 20

        # When a scale is applied, large points will tend to obfuscate small ones. Bringing the smaller
        # points to the front (by plotting them last) is a necessary intermediate step, which is what this bit of
        # code does.
        sorted_indices = np.array(sorted(enumerate(sizes), key=lambda tup: tup[1])[::-1])[:,0].astype(int)
        xs = np.array(xs)[sorted_indices]
        ys = np.array(ys)[sorted_indices]
        sizes = np.array(sizes)[sorted_indices]
        colors = np.array(colors)[sorted_indices]

        # Draw a legend, if appropriate.
        if legend and (legend_var == "scale" or hue is None):
            _paint_carto_legend(ax, scalar_values, legend_values, legend_labels, dscale, legend_kwargs)
    else:
        sizes = kwargs.pop('s') if 's' in kwargs.keys() else 20

    # Draw.
    if projection:
        ax.scatter(xs, ys, transform=ccrs.PlateCarree(), c=colors, s=sizes, **kwargs)
    else:
        ax.scatter(xs, ys, c=colors, s=sizes, **kwargs)

    return ax
Geospatial scatter plot.

    Parameters
    ----------
    df : GeoDataFrame
        The data being plotted.
    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>`_.
    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 `scatter plot
        <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.scatter>`_.

    Returns
    -------
    ``AxesSubplot`` or ``GeoAxesSubplot``
        The plot axis

    Examples
    --------

    The ``pointplot`` is a `geospatial scatter plot <https://en.wikipedia.org/wiki/Scatter_plot>`_ representing
    each observation in your dataset with a single point. It is simple and easily interpretable plot that is nearly
    universally understood, making it an ideal choice for showing simple pointwise relationships between
    observations.

    The expected input is a ``GeoDataFrame`` containing geometries of the ``shapely.geometry.Point`` type. A
    bare-bones pointplot goes thusly:

    .. code-block:: python

        import geoplot as gplt
        import geoplot.crs as gcrs
        gplt.pointplot(points)

    .. image:: ../figures/pointplot/pointplot-initial.png


    The ``hue`` parameter accepts a data column and applies a colormap to the output. The ``legend`` parameter
    toggles a legend.

    .. code-block:: python

        gplt.pointplot(cities, projection=gcrs.AlbersEqualArea(), hue='ELEV_IN_FT', legend=True)

    .. image:: ../figures/pointplot/pointplot-legend.png

    The ``pointplot`` binning methodology is controlled using by `scheme`` parameter. 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

        gplt.pointplot(cities, projection=gcrs.AlbersEqualArea(), hue='ELEV_IN_FT',
                       legend=True, scheme='equal_interval')

    .. image:: ../figures/pointplot/pointplot-scheme.png

    Alternatively, your data may already be `categorical
    <http://pandas.pydata.org/pandas-docs/stable/categorical.html>`_. In that case specify ``categorical=True`` instead.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(), hue='BOROUGH',
                       legend=True, categorical=True)

    .. image:: ../figures/pointplot/pointplot-categorical.png

    Keyword arguments can be passed to the legend using the ``legend_kwargs`` argument. These arguments will be
    passed to the underlying ``matplotlib.legend.Legend`` instance (`ref
    <http://matplotlib.org/api/legend_api.html#matplotlib.legend.Legend>`_). The ``loc`` and ``bbox_to_anchor``
    parameters are particularly useful for positioning the legend. Other additional arguments will be passed to the
    underlying ``matplotlib`` `scatter plot <http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.scatter>`_.

    .. code-block:: python

        gplt.pointplot(collisions[collisions['BOROUGH'].notnull()], projection=gcrs.AlbersEqualArea(),
                       hue='BOROUGH', categorical=True,
                       legend=True, legend_kwargs={'loc': 'upper left'},
                       edgecolor='white', linewidth=0.5)

    .. image:: ../figures/pointplot/pointplot-kwargs.png

    Change the number of bins by specifying an alternative ``k`` value. Adjust the `colormap
    <http://matplotlib.org/examples/color/colormaps_reference.html>`_ using the ``cmap`` parameter. To use a
    continuous colormap, explicitly specify ``k=None``. Note that if ``legend=True``, a ``matplotlib``
    `colorbar legend <http://matplotlib.org/api/colorbar_api.html>`_ will be used.

    .. code-block:: python

        gplt.pointplot(data, projection=gcrs.AlbersEqualArea(),
                       hue='var', k=8,
                       edgecolor='white', linewidth=0.5,
                       legend=True, legend_kwargs={'bbox_to_anchor': (1.25, 1.0)})

    .. image:: ../figures/pointplot/pointplot-k.png

    ``scale`` provides an alternative or additional visual variable.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED',
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-scale.png

    The limits can be adjusted to fit your data using the ``limits`` parameter.

    .. code-block:: python

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED', limits=(0, 10),
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-limits.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):
            def scalar(val):
                return 2
            return scalar

        gplt.pointplot(collisions, projection=gcrs.AlbersEqualArea(),
                       scale='NUMBER OF PERSONS INJURED', scale_func=trivial_scale,
                       legend=True, legend_kwargs={'loc': 'upper left'})

    .. image:: ../figures/pointplot/pointplot-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

        gplt.pointplot(collisions[collisions['BOROUGH'].notnull()],
                       projection=gcrs.AlbersEqualArea(),
                       hue='BOROUGH', categorical=True,
                       scale='NUMBER OF PERSONS INJURED', limits=(0, 10),
                       legend=True, legend_kwargs={'loc': 'upper left'},
                       legend_var='scale')

    .. image:: ../figures/pointplot/pointplot-legend-var.png