from __future__ import annotations

import functools
import warnings
from collections.abc import Hashable, Iterable, MutableMapping
from typing import TYPE_CHECKING, Any, Callable, Literal, cast, overload

import numpy as np
import pandas as pd

from xarray.core.alignment import broadcast
from xarray.core.concat import concat
from xarray.plot.facetgrid import _easy_facetgrid
from xarray.plot.utils import (
    _LINEWIDTH_RANGE,
    _MARKERSIZE_RANGE,
    _add_colorbar,
    _add_legend,
    _assert_valid_xy,
    _determine_guide,
    _ensure_plottable,
    _guess_coords_to_plot,
    _infer_interval_breaks,
    _infer_xy_labels,
    _Normalize,
    _process_cmap_cbar_kwargs,
    _rescale_imshow_rgb,
    _resolve_intervals_1dplot,
    _resolve_intervals_2dplot,
    _update_axes,
    get_axis,
    import_matplotlib_pyplot,
    label_from_attrs,
)

if TYPE_CHECKING:
    from matplotlib.axes import Axes
    from matplotlib.collections import PathCollection, QuadMesh
    from matplotlib.colors import Colormap, Normalize
    from matplotlib.container import BarContainer
    from matplotlib.contour import QuadContourSet
    from matplotlib.image import AxesImage
    from mpl_toolkits.mplot3d.art3d import Line3D, Poly3DCollection
    from numpy.typing import ArrayLike

    from xarray.core.dataarray import DataArray
    from xarray.core.types import (
        AspectOptions,
        ExtendOptions,
        HueStyleOptions,
        ScaleOptions,
        T_DataArray,
    )
    from xarray.plot.facetgrid import FacetGrid

_styles: MutableMapping[str, Any] = {
    # Add a white border to make it easier seeing overlapping markers:
    "scatter.edgecolors": "w",
}


def _infer_line_data(
    darray: DataArray, x: Hashable | None, y: Hashable | None, hue: Hashable | None
) -> tuple[DataArray, DataArray, DataArray | None, str]:
    ndims = len(darray.dims)

    if x is not None and y is not None:
        raise ValueError("Cannot specify both x and y kwargs for line plots.")

    if x is not None:
        _assert_valid_xy(darray, x, "x")

    if y is not None:
        _assert_valid_xy(darray, y, "y")

    if ndims == 1:
        huename = None
        hueplt = None
        huelabel = ""

        if x is not None:
            xplt = darray[x]
            yplt = darray

        elif y is not None:
            xplt = darray
            yplt = darray[y]

        else:  # Both x & y are None
            dim = darray.dims[0]
            xplt = darray[dim]
            yplt = darray

    else:
        if x is None and y is None and hue is None:
            raise ValueError("For 2D inputs, please specify either hue, x or y.")

        if y is None:
            if hue is not None:
                _assert_valid_xy(darray, hue, "hue")
            xname, huename = _infer_xy_labels(darray=darray, x=x, y=hue)
            xplt = darray[xname]
            if xplt.ndim > 1:
                if huename in darray.dims:
                    otherindex = 1 if darray.dims.index(huename) == 0 else 0
                    otherdim = darray.dims[otherindex]
                    yplt = darray.transpose(otherdim, huename, transpose_coords=False)
                    xplt = xplt.transpose(otherdim, huename, transpose_coords=False)
                else:
                    raise ValueError(
                        "For 2D inputs, hue must be a dimension"
                        " i.e. one of " + repr(darray.dims)
                    )

            else:
                (xdim,) = darray[xname].dims
                (huedim,) = darray[huename].dims
                yplt = darray.transpose(xdim, huedim)

        else:
            yname, huename = _infer_xy_labels(darray=darray, x=y, y=hue)
            yplt = darray[yname]
            if yplt.ndim > 1:
                if huename in darray.dims:
                    otherindex = 1 if darray.dims.index(huename) == 0 else 0
                    otherdim = darray.dims[otherindex]
                    xplt = darray.transpose(otherdim, huename, transpose_coords=False)
                    yplt = yplt.transpose(otherdim, huename, transpose_coords=False)
                else:
                    raise ValueError(
                        "For 2D inputs, hue must be a dimension"
                        " i.e. one of " + repr(darray.dims)
                    )

            else:
                (ydim,) = darray[yname].dims
                (huedim,) = darray[huename].dims
                xplt = darray.transpose(ydim, huedim)

        huelabel = label_from_attrs(darray[huename])
        hueplt = darray[huename]

    return xplt, yplt, hueplt, huelabel


def _prepare_plot1d_data(
    darray: T_DataArray,
    coords_to_plot: MutableMapping[str, Hashable],
    plotfunc_name: str | None = None,
    _is_facetgrid: bool = False,
) -> dict[str, T_DataArray]:
    """
    Prepare data for usage with plt.scatter.

    Parameters
    ----------
    darray : T_DataArray
        Base DataArray.
    coords_to_plot : MutableMapping[str, Hashable]
        Coords that will be plotted.
    plotfunc_name : str | None
        Name of the plotting function that will be used.

    Returns
    -------
    plts : dict[str, T_DataArray]
        Dict of DataArrays that will be sent to matplotlib.

    Examples
    --------
    >>> # Make sure int coords are plotted:
    >>> a = xr.DataArray(
    ...     data=[1, 2],
    ...     coords={1: ("x", [0, 1], {"units": "s"})},
    ...     dims=("x",),
    ...     name="a",
    ... )
    >>> plts = xr.plot.dataarray_plot._prepare_plot1d_data(
    ...     a, coords_to_plot={"x": 1, "z": None, "hue": None, "size": None}
    ... )
    >>> # Check which coords to plot:
    >>> print({k: v.name for k, v in plts.items()})
    {'y': 'a', 'x': 1}
    """
    # If there are more than 1 dimension in the array than stack all the
    # dimensions so the plotter can plot anything:
    if darray.ndim > 1:
        # When stacking dims the lines will continue connecting. For floats
        # this can be solved by adding a nan element inbetween the flattening
        # points:
        dims_T = []
        if np.issubdtype(darray.dtype, np.floating):
            for v in ["z", "x"]:
                dim = coords_to_plot.get(v, None)
                if (dim is not None) and (dim in darray.dims):
                    darray_nan = np.nan * darray.isel({dim: -1})
                    darray = concat([darray, darray_nan], dim=dim)
                    dims_T.append(coords_to_plot[v])

        # Lines should never connect to the same coordinate when stacked,
        # transpose to avoid this as much as possible:
        darray = darray.transpose(..., *dims_T)

        # Array is now ready to be stacked:
        darray = darray.stack(_stacked_dim=darray.dims)

    # Broadcast together all the chosen variables:
    plts = dict(y=darray)
    plts.update(
        {k: darray.coords[v] for k, v in coords_to_plot.items() if v is not None}
    )
    plts = dict(zip(plts.keys(), broadcast(*(plts.values()))))

    return plts


# return type is Any due to the many different possibilities
def plot(
    darray: DataArray,
    *,
    row: Hashable | None = None,
    col: Hashable | None = None,
    col_wrap: int | None = None,
    ax: Axes | None = None,
    hue: Hashable | None = None,
    subplot_kws: dict[str, Any] | None = None,
    **kwargs: Any,
) -> Any:
    """
    Default plot of DataArray using :py:mod:`matplotlib:matplotlib.pyplot`.

    Calls xarray plotting function based on the dimensions of
    the squeezed DataArray.

    =============== ===========================
    Dimensions      Plotting function
    =============== ===========================
    1               :py:func:`xarray.plot.line`
    2               :py:func:`xarray.plot.pcolormesh`
    Anything else   :py:func:`xarray.plot.hist`
    =============== ===========================

    Parameters
    ----------
    darray : DataArray
    row : Hashable or None, optional
        If passed, make row faceted plots on this dimension name.
    col : Hashable or None, optional
        If passed, make column faceted plots on this dimension name.
    col_wrap : int or None, optional
        Use together with ``col`` to wrap faceted plots.
    ax : matplotlib axes object, optional
        Axes on which to plot. By default, use the current axes.
        Mutually exclusive with ``size``, ``figsize`` and facets.
    hue : Hashable or None, optional
        If passed, make faceted line plots with hue on this dimension name.
    subplot_kws : dict, optional
        Dictionary of keyword arguments for Matplotlib subplots
        (see :py:meth:`matplotlib:matplotlib.figure.Figure.add_subplot`).
    **kwargs : optional
        Additional keyword arguments for Matplotlib.

    See Also
    --------
    xarray.DataArray.squeeze
    """
    darray = darray.squeeze().compute()

    plot_dims = set(darray.dims)
    plot_dims.discard(row)
    plot_dims.discard(col)
    plot_dims.discard(hue)

    ndims = len(plot_dims)

    plotfunc: Callable

    if ndims == 0 or darray.size == 0:
        raise TypeError("No numeric data to plot.")
    if ndims in (1, 2):
        if row or col:
            kwargs["subplot_kws"] = subplot_kws
            kwargs["row"] = row
            kwargs["col"] = col
            kwargs["col_wrap"] = col_wrap
        if ndims == 1:
            plotfunc = line
            kwargs["hue"] = hue
        elif ndims == 2:
            if hue:
                plotfunc = line
                kwargs["hue"] = hue
            else:
                plotfunc = pcolormesh
                kwargs["subplot_kws"] = subplot_kws
    else:
        if row or col or hue:
            raise ValueError(
                "Only 1d and 2d plots are supported for facets in xarray. "
                "See the package `Seaborn` for more options."
            )
        plotfunc = hist

    kwargs["ax"] = ax

    return plotfunc(darray, **kwargs)


@overload
def line(  # type: ignore[misc]  # None is hashable :(
    darray: DataArray,
    *args: Any,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    figsize: Iterable[float] | None = None,
    aspect: AspectOptions = None,
    size: float | None = None,
    ax: Axes | None = None,
    hue: Hashable | None = None,
    x: Hashable | None = None,
    y: Hashable | None = None,
    xincrease: bool | None = None,
    yincrease: bool | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    add_legend: bool = True,
    _labels: bool = True,
    **kwargs: Any,
) -> list[Line3D]:
    ...


@overload
def line(
    darray,
    *args: Any,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    figsize: Iterable[float] | None = None,
    aspect: AspectOptions = None,
    size: float | None = None,
    ax: Axes | None = None,
    hue: Hashable | None = None,
    x: Hashable | None = None,
    y: Hashable | None = None,
    xincrease: bool | None = None,
    yincrease: bool | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    add_legend: bool = True,
    _labels: bool = True,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def line(
    darray,
    *args: Any,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    figsize: Iterable[float] | None = None,
    aspect: AspectOptions = None,
    size: float | None = None,
    ax: Axes | None = None,
    hue: Hashable | None = None,
    x: Hashable | None = None,
    y: Hashable | None = None,
    xincrease: bool | None = None,
    yincrease: bool | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    add_legend: bool = True,
    _labels: bool = True,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


# This function signature should not change so that it can use
# matplotlib format strings
def line(
    darray: DataArray,
    *args: Any,
    row: Hashable | None = None,
    col: Hashable | None = None,
    figsize: Iterable[float] | None = None,
    aspect: AspectOptions = None,
    size: float | None = None,
    ax: Axes | None = None,
    hue: Hashable | None = None,
    x: Hashable | None = None,
    y: Hashable | None = None,
    xincrease: bool | None = None,
    yincrease: bool | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    add_legend: bool = True,
    _labels: bool = True,
    **kwargs: Any,
) -> list[Line3D] | FacetGrid[DataArray]:
    """
    Line plot of DataArray values.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.plot`.

    Parameters
    ----------
    darray : DataArray
        Either 1D or 2D. If 2D, one of ``hue``, ``x`` or ``y`` must be provided.
    row : Hashable, optional
        If passed, make row faceted plots on this dimension name.
    col : Hashable, optional
        If passed, make column faceted plots on this dimension name.
    figsize : tuple, optional
        A tuple (width, height) of the figure in inches.
        Mutually exclusive with ``size`` and ``ax``.
    aspect : "auto", "equal", scalar or None, optional
        Aspect ratio of plot, so that ``aspect * size`` gives the *width* in
        inches. Only used if a ``size`` is provided.
    size : scalar, optional
        If provided, create a new figure for the plot with the given size:
        *height* (in inches) of each plot. See also: ``aspect``.
    ax : matplotlib axes object, optional
        Axes on which to plot. By default, the current is used.
        Mutually exclusive with ``size`` and ``figsize``.
    hue : Hashable, optional
        Dimension or coordinate for which you want multiple lines plotted.
        If plotting against a 2D coordinate, ``hue`` must be a dimension.
    x, y : Hashable, optional
        Dimension, coordinate or multi-index level for *x*, *y* axis.
        Only one of these may be specified.
        The other will be used for values from the DataArray on which this
        plot method is called.
    xincrease : bool or None, optional
        Should the values on the *x* axis be increasing from left to right?
        if ``None``, use the default for the Matplotlib function.
    yincrease : bool or None, optional
        Should the values on the *y* axis be increasing from top to bottom?
        if ``None``, use the default for the Matplotlib function.
    xscale, yscale : {'linear', 'symlog', 'log', 'logit'}, optional
        Specifies scaling for the *x*- and *y*-axis, respectively.
    xticks, yticks : array-like, optional
        Specify tick locations for *x*- and *y*-axis.
    xlim, ylim : array-like, optional
        Specify *x*- and *y*-axis limits.
    add_legend : bool, default: True
        Add legend with *y* axis coordinates (2D inputs only).
    *args, **kwargs : optional
        Additional arguments to :py:func:`matplotlib:matplotlib.pyplot.plot`.

    Returns
    -------
    primitive : list of Line3D or FacetGrid
        When either col or row is given, returns a FacetGrid, otherwise
        a list of matplotlib Line3D objects.
    """
    # Handle facetgrids first
    if row or col:
        allargs = locals().copy()
        allargs.update(allargs.pop("kwargs"))
        allargs.pop("darray")
        return _easy_facetgrid(darray, line, kind="line", **allargs)

    ndims = len(darray.dims)
    if ndims == 0 or darray.size == 0:
        # TypeError to be consistent with pandas
        raise TypeError("No numeric data to plot.")
    if ndims > 2:
        raise ValueError(
            "Line plots are for 1- or 2-dimensional DataArrays. "
            "Passed DataArray has {ndims} "
            "dimensions".format(ndims=ndims)
        )

    # The allargs dict passed to _easy_facetgrid above contains args
    if args == ():
        args = kwargs.pop("args", ())
    else:
        assert "args" not in kwargs

    ax = get_axis(figsize, size, aspect, ax)
    xplt, yplt, hueplt, hue_label = _infer_line_data(darray, x, y, hue)

    # Remove pd.Intervals if contained in xplt.values and/or yplt.values.
    xplt_val, yplt_val, x_suffix, y_suffix, kwargs = _resolve_intervals_1dplot(
        xplt.to_numpy(), yplt.to_numpy(), kwargs
    )
    xlabel = label_from_attrs(xplt, extra=x_suffix)
    ylabel = label_from_attrs(yplt, extra=y_suffix)

    _ensure_plottable(xplt_val, yplt_val)

    primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs)

    if _labels:
        if xlabel is not None:
            ax.set_xlabel(xlabel)

        if ylabel is not None:
            ax.set_ylabel(ylabel)

        ax.set_title(darray._title_for_slice())

    if darray.ndim == 2 and add_legend:
        assert hueplt is not None
        ax.legend(handles=primitive, labels=list(hueplt.to_numpy()), title=hue_label)

    # Rotate dates on xlabels
    # Do this without calling autofmt_xdate so that x-axes ticks
    # on other subplots (if any) are not deleted.
    # https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
    if np.issubdtype(xplt.dtype, np.datetime64):
        for xlabels in ax.get_xticklabels():
            xlabels.set_rotation(30)
            xlabels.set_horizontalalignment("right")

    _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim)

    return primitive


@overload
def step(  # type: ignore[misc]  # None is hashable :(
    darray: DataArray,
    *args: Any,
    where: Literal["pre", "post", "mid"] = "pre",
    drawstyle: str | None = None,
    ds: str | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    **kwargs: Any,
) -> list[Line3D]:
    ...


@overload
def step(
    darray: DataArray,
    *args: Any,
    where: Literal["pre", "post", "mid"] = "pre",
    drawstyle: str | None = None,
    ds: str | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def step(
    darray: DataArray,
    *args: Any,
    where: Literal["pre", "post", "mid"] = "pre",
    drawstyle: str | None = None,
    ds: str | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


def step(
    darray: DataArray,
    *args: Any,
    where: Literal["pre", "post", "mid"] = "pre",
    drawstyle: str | None = None,
    ds: str | None = None,
    row: Hashable | None = None,
    col: Hashable | None = None,
    **kwargs: Any,
) -> list[Line3D] | FacetGrid[DataArray]:
    """
    Step plot of DataArray values.

    Similar to :py:func:`matplotlib:matplotlib.pyplot.step`.

    Parameters
    ----------
    where : {'pre', 'post', 'mid'}, default: 'pre'
        Define where the steps should be placed:

        - ``'pre'``: The y value is continued constantly to the left from
          every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
          value ``y[i]``.
        - ``'post'``: The y value is continued constantly to the right from
          every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
          value ``y[i]``.
        - ``'mid'``: Steps occur half-way between the *x* positions.

        Note that this parameter is ignored if one coordinate consists of
        :py:class:`pandas.Interval` values, e.g. as a result of
        :py:func:`xarray.Dataset.groupby_bins`. In this case, the actual
        boundaries of the interval are used.
    drawstyle, ds : str or None, optional
        Additional drawstyle. Only use one of drawstyle and ds.
    row : Hashable, optional
        If passed, make row faceted plots on this dimension name.
    col : Hashable, optional
        If passed, make column faceted plots on this dimension name.
    *args, **kwargs : optional
        Additional arguments for :py:func:`xarray.plot.line`.

    Returns
    -------
    primitive : list of Line3D or FacetGrid
        When either col or row is given, returns a FacetGrid, otherwise
        a list of matplotlib Line3D objects.
    """
    if where not in {"pre", "post", "mid"}:
        raise ValueError("'where' argument to step must be 'pre', 'post' or 'mid'")

    if ds is not None:
        if drawstyle is None:
            drawstyle = ds
        else:
            raise TypeError("ds and drawstyle are mutually exclusive")
    if drawstyle is None:
        drawstyle = ""
    drawstyle = "steps-" + where + drawstyle

    return line(darray, *args, drawstyle=drawstyle, col=col, row=row, **kwargs)


def hist(
    darray: DataArray,
    *args: Any,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    xincrease: bool | None = None,
    yincrease: bool | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    **kwargs: Any,
) -> tuple[np.ndarray, np.ndarray, BarContainer]:
    """
    Histogram of DataArray.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.hist`.

    Plots *N*-dimensional arrays by first flattening the array.

    Parameters
    ----------
    darray : DataArray
        Can have any number of dimensions.
    figsize : Iterable of float, optional
        A tuple (width, height) of the figure in inches.
        Mutually exclusive with ``size`` and ``ax``.
    aspect : "auto", "equal", scalar or None, optional
        Aspect ratio of plot, so that ``aspect * size`` gives the *width* in
        inches. Only used if a ``size`` is provided.
    size : scalar, optional
        If provided, create a new figure for the plot with the given size:
        *height* (in inches) of each plot. See also: ``aspect``.
    ax : matplotlib axes object, optional
        Axes on which to plot. By default, use the current axes.
        Mutually exclusive with ``size`` and ``figsize``.
    xincrease : bool or None, optional
        Should the values on the *x* axis be increasing from left to right?
        if ``None``, use the default for the Matplotlib function.
    yincrease : bool or None, optional
        Should the values on the *y* axis be increasing from top to bottom?
        if ``None``, use the default for the Matplotlib function.
    xscale, yscale : {'linear', 'symlog', 'log', 'logit'}, optional
        Specifies scaling for the *x*- and *y*-axis, respectively.
    xticks, yticks : array-like, optional
        Specify tick locations for *x*- and *y*-axis.
    xlim, ylim : array-like, optional
        Specify *x*- and *y*-axis limits.
    **kwargs : optional
        Additional keyword arguments to :py:func:`matplotlib:matplotlib.pyplot.hist`.

    """
    assert len(args) == 0

    if darray.ndim == 0 or darray.size == 0:
        # TypeError to be consistent with pandas
        raise TypeError("No numeric data to plot.")

    ax = get_axis(figsize, size, aspect, ax)

    no_nan = np.ravel(darray.to_numpy())
    no_nan = no_nan[pd.notnull(no_nan)]

    primitive = ax.hist(no_nan, **kwargs)

    ax.set_title(darray._title_for_slice())
    ax.set_xlabel(label_from_attrs(darray))

    _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim)

    return primitive


def _plot1d(plotfunc):
    """Decorator for common 1d plotting logic."""
    commondoc = """
    Parameters
    ----------
    darray : DataArray
        Must be 2 dimensional, unless creating faceted plots.
    x : Hashable or None, optional
        Coordinate for x axis. If None use darray.dims[1].
    y : Hashable or None, optional
        Coordinate for y axis. If None use darray.dims[0].
    z : Hashable or None, optional
        If specified plot 3D and use this coordinate for *z* axis.
    hue : Hashable or None, optional
        Dimension or coordinate for which you want multiple lines plotted.
    hue_style: {'discrete', 'continuous'} or None, optional
        How to use the ``hue`` variable:

        - ``'continuous'`` -- continuous color scale
          (default for numeric ``hue`` variables)
        - ``'discrete'`` -- a color for each unique value,
          using the default color cycle
          (default for non-numeric ``hue`` variables)

    markersize: Hashable or None, optional
        scatter only. Variable by which to vary size of scattered points.
    linewidth: Hashable or None, optional
        Variable by which to vary linewidth.
    row : Hashable, optional
        If passed, make row faceted plots on this dimension name.
    col : Hashable, optional
        If passed, make column faceted plots on this dimension name.
    col_wrap : int, optional
        Use together with ``col`` to wrap faceted plots
    ax : matplotlib axes object, optional
        If None, uses the current axis. Not applicable when using facets.
    figsize : Iterable[float] or None, optional
        A tuple (width, height) of the figure in inches.
        Mutually exclusive with ``size`` and ``ax``.
    size : scalar, optional
        If provided, create a new figure for the plot with the given size.
        Height (in inches) of each plot. See also: ``aspect``.
    aspect : "auto", "equal", scalar or None, optional
        Aspect ratio of plot, so that ``aspect * size`` gives the width in
        inches. Only used if a ``size`` is provided.
    xincrease : bool or None, default: True
        Should the values on the x axes be increasing from left to right?
        if None, use the default for the matplotlib function.
    yincrease : bool or None, default: True
        Should the values on the y axes be increasing from top to bottom?
        if None, use the default for the matplotlib function.
    add_legend : bool or None, optional
        If True use xarray metadata to add a legend.
    add_colorbar : bool or None, optional
        If True add a colorbar.
    add_labels : bool or None, optional
        If True use xarray metadata to label axes
    add_title : bool or None, optional
        If True use xarray metadata to add a title
    subplot_kws : dict, optional
        Dictionary of keyword arguments for matplotlib subplots. Only applies
        to FacetGrid plotting.
    xscale : {'linear', 'symlog', 'log', 'logit'} or None, optional
        Specifies scaling for the x-axes.
    yscale : {'linear', 'symlog', 'log', 'logit'} or None, optional
        Specifies scaling for the y-axes.
    xticks : ArrayLike or None, optional
        Specify tick locations for x-axes.
    yticks : ArrayLike or None, optional
        Specify tick locations for y-axes.
    xlim : ArrayLike or None, optional
        Specify x-axes limits.
    ylim : ArrayLike or None, optional
        Specify y-axes limits.
    cmap : matplotlib colormap name or colormap, optional
        The mapping from data values to color space. Either a
        Matplotlib colormap name or object. If not provided, this will
        be either ``'viridis'`` (if the function infers a sequential
        dataset) or ``'RdBu_r'`` (if the function infers a diverging
        dataset).
        See :doc:`Choosing Colormaps in Matplotlib <matplotlib:tutorials/colors/colormaps>`
        for more information.

        If *seaborn* is installed, ``cmap`` may also be a
        `seaborn color palette <https://seaborn.pydata.org/tutorial/color_palettes.html>`_.
        Note: if ``cmap`` is a seaborn color palette,
        ``levels`` must also be specified.
    vmin : float or None, optional
        Lower value to anchor the colormap, otherwise it is inferred from the
        data and other keyword arguments. When a diverging dataset is inferred,
        setting `vmin` or `vmax` will fix the other by symmetry around
        ``center``. Setting both values prevents use of a diverging colormap.
        If discrete levels are provided as an explicit list, both of these
        values are ignored.
    vmax : float or None, optional
        Upper value to anchor the colormap, otherwise it is inferred from the
        data and other keyword arguments. When a diverging dataset is inferred,
        setting `vmin` or `vmax` will fix the other by symmetry around
        ``center``. Setting both values prevents use of a diverging colormap.
        If discrete levels are provided as an explicit list, both of these
        values are ignored.
    norm : matplotlib.colors.Normalize, optional
        If ``norm`` has ``vmin`` or ``vmax`` specified, the corresponding
        kwarg must be ``None``.
    extend : {'neither', 'both', 'min', 'max'}, optional
        How to draw arrows extending the colorbar beyond its limits. If not
        provided, ``extend`` is inferred from ``vmin``, ``vmax`` and the data limits.
    levels : int or array-like, optional
        Split the colormap (``cmap``) into discrete color intervals. If an integer
        is provided, "nice" levels are chosen based on the data range: this can
        imply that the final number of levels is not exactly the expected one.
        Setting ``vmin`` and/or ``vmax`` with ``levels=N`` is equivalent to
        setting ``levels=np.linspace(vmin, vmax, N)``.
    **kwargs : optional
        Additional arguments to wrapped matplotlib function

    Returns
    -------
    artist :
        The same type of primitive artist that the wrapped matplotlib
        function returns
    """

    # Build on the original docstring
    plotfunc.__doc__ = f"{plotfunc.__doc__}\n{commondoc}"

    @functools.wraps(
        plotfunc, assigned=("__module__", "__name__", "__qualname__", "__doc__")
    )
    def newplotfunc(
        darray: DataArray,
        *args: Any,
        x: Hashable | None = None,
        y: Hashable | None = None,
        z: Hashable | None = None,
        hue: Hashable | None = None,
        hue_style: HueStyleOptions = None,
        markersize: Hashable | None = None,
        linewidth: Hashable | None = None,
        row: Hashable | None = None,
        col: Hashable | None = None,
        col_wrap: int | None = None,
        ax: Axes | None = None,
        figsize: Iterable[float] | None = None,
        size: float | None = None,
        aspect: float | None = None,
        xincrease: bool | None = True,
        yincrease: bool | None = True,
        add_legend: bool | None = None,
        add_colorbar: bool | None = None,
        add_labels: bool | Iterable[bool] = True,
        add_title: bool = True,
        subplot_kws: dict[str, Any] | None = None,
        xscale: ScaleOptions = None,
        yscale: ScaleOptions = None,
        xticks: ArrayLike | None = None,
        yticks: ArrayLike | None = None,
        xlim: ArrayLike | None = None,
        ylim: ArrayLike | None = None,
        cmap: str | Colormap | None = None,
        vmin: float | None = None,
        vmax: float | None = None,
        norm: Normalize | None = None,
        extend: ExtendOptions = None,
        levels: ArrayLike | None = None,
        **kwargs,
    ) -> Any:
        # All 1d plots in xarray share this function signature.
        # Method signature below should be consistent.

        plt = import_matplotlib_pyplot()

        if subplot_kws is None:
            subplot_kws = dict()

        # Handle facetgrids first
        if row or col:
            if z is not None:
                subplot_kws.update(projection="3d")

            allargs = locals().copy()
            allargs.update(allargs.pop("kwargs"))
            allargs.pop("darray")
            allargs.pop("plt")
            allargs["plotfunc"] = globals()[plotfunc.__name__]

            return _easy_facetgrid(darray, kind="plot1d", **allargs)

        if darray.ndim == 0 or darray.size == 0:
            # TypeError to be consistent with pandas
            raise TypeError("No numeric data to plot.")

        # The allargs dict passed to _easy_facetgrid above contains args
        if args == ():
            args = kwargs.pop("args", ())

        if args:
            assert "args" not in kwargs
            # TODO: Deprecated since 2022.10:
            msg = "Using positional arguments is deprecated for plot methods, use keyword arguments instead."
            assert x is None
            x = args[0]
            if len(args) > 1:
                assert y is None
                y = args[1]
            if len(args) > 2:
                assert z is None
                z = args[2]
            if len(args) > 3:
                assert hue is None
                hue = args[3]
            if len(args) > 4:
                raise ValueError(msg)
            else:
                warnings.warn(msg, DeprecationWarning, stacklevel=2)
        del args

        _is_facetgrid = kwargs.pop("_is_facetgrid", False)

        if plotfunc.__name__ == "scatter":
            size_ = kwargs.pop("_size", markersize)
            size_r = _MARKERSIZE_RANGE
        else:
            size_ = kwargs.pop("_size", linewidth)
            size_r = _LINEWIDTH_RANGE

        # Get data to plot:
        coords_to_plot: MutableMapping[str, Hashable | None] = dict(
            x=x, z=z, hue=hue, size=size_
        )
        if not _is_facetgrid:
            # Guess what coords to use if some of the values in coords_to_plot are None:
            coords_to_plot = _guess_coords_to_plot(darray, coords_to_plot, kwargs)
        plts = _prepare_plot1d_data(darray, coords_to_plot, plotfunc.__name__)
        xplt = plts.pop("x", None)
        yplt = plts.pop("y", None)
        zplt = plts.pop("z", None)
        kwargs.update(zplt=zplt)
        hueplt = plts.pop("hue", None)
        sizeplt = plts.pop("size", None)

        # Handle size and hue:
        hueplt_norm = _Normalize(data=hueplt)
        kwargs.update(hueplt=hueplt_norm.values)
        sizeplt_norm = _Normalize(
            data=sizeplt, width=size_r, _is_facetgrid=_is_facetgrid
        )
        kwargs.update(sizeplt=sizeplt_norm.values)
        cmap_params_subset = kwargs.pop("cmap_params_subset", {})
        cbar_kwargs = kwargs.pop("cbar_kwargs", {})

        if hueplt_norm.data is not None:
            if not hueplt_norm.data_is_numeric:
                # Map hue values back to its original value:
                cbar_kwargs.update(format=hueplt_norm.format, ticks=hueplt_norm.ticks)
                levels = kwargs.get("levels", hueplt_norm.levels)

            cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs(
                plotfunc,
                cast("DataArray", hueplt_norm.values).data,
                **locals(),
            )

            # subset that can be passed to scatter, hist2d
            if not cmap_params_subset:
                ckw = {vv: cmap_params[vv] for vv in ("vmin", "vmax", "norm", "cmap")}
                cmap_params_subset.update(**ckw)

        with plt.rc_context(_styles):
            if z is not None:
                if ax is None:
                    subplot_kws.update(projection="3d")
                ax = get_axis(figsize, size, aspect, ax, **subplot_kws)
                # Using 30, 30 minimizes rotation of the plot. Making it easier to
                # build on your intuition from 2D plots:
                ax.view_init(azim=30, elev=30, vertical_axis="y")
            else:
                ax = get_axis(figsize, size, aspect, ax, **subplot_kws)

            primitive = plotfunc(
                xplt,
                yplt,
                ax=ax,
                add_labels=add_labels,
                **cmap_params_subset,
                **kwargs,
            )

        if np.any(np.asarray(add_labels)) and add_title:
            ax.set_title(darray._title_for_slice())

        add_colorbar_, add_legend_ = _determine_guide(
            hueplt_norm,
            sizeplt_norm,
            add_colorbar,
            add_legend,
            plotfunc_name=plotfunc.__name__,
        )

        if add_colorbar_:
            if "label" not in cbar_kwargs:
                cbar_kwargs["label"] = label_from_attrs(hueplt_norm.data)

            _add_colorbar(
                primitive, ax, kwargs.get("cbar_ax", None), cbar_kwargs, cmap_params
            )

        if add_legend_:
            if plotfunc.__name__ in ["scatter", "line"]:
                _add_legend(
                    hueplt_norm
                    if add_legend or not add_colorbar_
                    else _Normalize(None),
                    sizeplt_norm,
                    primitive,
                    legend_ax=ax,
                    plotfunc=plotfunc.__name__,
                )
            else:
                hueplt_norm_values: list[np.ndarray | None]
                if hueplt_norm.data is not None:
                    hueplt_norm_values = list(
                        cast("DataArray", hueplt_norm.data).to_numpy()
                    )
                else:
                    hueplt_norm_values = [hueplt_norm.data]

                if plotfunc.__name__ == "hist":
                    ax.legend(
                        handles=primitive[-1],
                        labels=hueplt_norm_values,
                        title=label_from_attrs(hueplt_norm.data),
                    )
                else:
                    ax.legend(
                        handles=primitive,
                        labels=hueplt_norm_values,
                        title=label_from_attrs(hueplt_norm.data),
                    )

        _update_axes(
            ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim
        )

        return primitive

    # we want to actually expose the signature of newplotfunc
    # and not the copied **kwargs from the plotfunc which
    # functools.wraps adds, so delete the wrapped attr
    del newplotfunc.__wrapped__

    return newplotfunc


def _add_labels(
    add_labels: bool | Iterable[bool],
    darrays: Iterable[DataArray],
    suffixes: Iterable[str],
    rotate_labels: Iterable[bool],
    ax: Axes,
) -> None:
    # Set x, y, z labels:
    add_labels = [add_labels] * 3 if isinstance(add_labels, bool) else add_labels
    for axis, add_label, darray, suffix, rotate_label in zip(
        ("x", "y", "z"), add_labels, darrays, suffixes, rotate_labels
    ):
        if darray is None:
            continue

        if add_label:
            label = label_from_attrs(darray, extra=suffix)
            if label is not None:
                getattr(ax, f"set_{axis}label")(label)

        if rotate_label and np.issubdtype(darray.dtype, np.datetime64):
            # Rotate dates on xlabels
            # Do this without calling autofmt_xdate so that x-axes ticks
            # on other subplots (if any) are not deleted.
            # https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
            for labels in getattr(ax, f"get_{axis}ticklabels")():
                labels.set_rotation(30)
                labels.set_horizontalalignment("right")


@overload
def scatter(
    darray: DataArray,
    *args: Any,
    x: Hashable | None = None,
    y: Hashable | None = None,
    z: Hashable | None = None,
    hue: Hashable | None = None,
    hue_style: HueStyleOptions = None,
    markersize: Hashable | None = None,
    linewidth: Hashable | None = None,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: float | None = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_legend: bool | None = None,
    add_colorbar: bool | None = None,
    add_labels: bool | Iterable[bool] = True,
    add_title: bool = True,
    subplot_kws: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    cmap: str | Colormap | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    norm: Normalize | None = None,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    **kwargs,
) -> PathCollection:
    ...


@overload
def scatter(
    darray: DataArray,
    *args: Any,
    x: Hashable | None = None,
    y: Hashable | None = None,
    z: Hashable | None = None,
    hue: Hashable | None = None,
    hue_style: HueStyleOptions = None,
    markersize: Hashable | None = None,
    linewidth: Hashable | None = None,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: float | None = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_legend: bool | None = None,
    add_colorbar: bool | None = None,
    add_labels: bool | Iterable[bool] = True,
    add_title: bool = True,
    subplot_kws: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    cmap: str | Colormap | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    norm: Normalize | None = None,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    **kwargs,
) -> FacetGrid[DataArray]:
    ...


@overload
def scatter(
    darray: DataArray,
    *args: Any,
    x: Hashable | None = None,
    y: Hashable | None = None,
    z: Hashable | None = None,
    hue: Hashable | None = None,
    hue_style: HueStyleOptions = None,
    markersize: Hashable | None = None,
    linewidth: Hashable | None = None,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: float | None = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_legend: bool | None = None,
    add_colorbar: bool | None = None,
    add_labels: bool | Iterable[bool] = True,
    add_title: bool = True,
    subplot_kws: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    cmap: str | Colormap | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    norm: Normalize | None = None,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    **kwargs,
) -> FacetGrid[DataArray]:
    ...


@_plot1d
def scatter(
    xplt: DataArray | None,
    yplt: DataArray | None,
    ax: Axes,
    add_labels: bool | Iterable[bool] = True,
    **kwargs,
) -> PathCollection:
    """Scatter variables against each other.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.scatter`.
    """
    if "u" in kwargs or "v" in kwargs:
        raise ValueError("u, v are not allowed in scatter plots.")

    zplt: DataArray | None = kwargs.pop("zplt", None)
    hueplt: DataArray | None = kwargs.pop("hueplt", None)
    sizeplt: DataArray | None = kwargs.pop("sizeplt", None)

    if hueplt is not None:
        kwargs.update(c=hueplt.to_numpy().ravel())

    if sizeplt is not None:
        kwargs.update(s=sizeplt.to_numpy().ravel())

    axis_order = ["x", "y", "z"]

    plts_dict: dict[str, DataArray | None] = dict(x=xplt, y=yplt, z=zplt)
    plts_or_none = [plts_dict[v] for v in axis_order]
    plts = [p for p in plts_or_none if p is not None]
    primitive = ax.scatter(*[p.to_numpy().ravel() for p in plts], **kwargs)
    _add_labels(add_labels, plts, ("", "", ""), (True, False, False), ax)

    return primitive


def _plot2d(plotfunc):
    """Decorator for common 2d plotting logic."""
    commondoc = """
    Parameters
    ----------
    darray : DataArray
        Must be two-dimensional, unless creating faceted plots.
    x : Hashable or None, optional
        Coordinate for *x* axis. If ``None``, use ``darray.dims[1]``.
    y : Hashable or None, optional
        Coordinate for *y* axis. If ``None``, use ``darray.dims[0]``.
    figsize : Iterable or float or None, optional
        A tuple (width, height) of the figure in inches.
        Mutually exclusive with ``size`` and ``ax``.
    size : scalar, optional
        If provided, create a new figure for the plot with the given size:
        *height* (in inches) of each plot. See also: ``aspect``.
    aspect : "auto", "equal", scalar or None, optional
        Aspect ratio of plot, so that ``aspect * size`` gives the *width* in
        inches. Only used if a ``size`` is provided.
    ax : matplotlib axes object, optional
        Axes on which to plot. By default, use the current axes.
        Mutually exclusive with ``size`` and ``figsize``.
    row : Hashable or None, optional
        If passed, make row faceted plots on this dimension name.
    col : Hashable or None, optional
        If passed, make column faceted plots on this dimension name.
    col_wrap : int, optional
        Use together with ``col`` to wrap faceted plots.
    xincrease : None, True, or False, optional
        Should the values on the *x* axis be increasing from left to right?
        If ``None``, use the default for the Matplotlib function.
    yincrease : None, True, or False, optional
        Should the values on the *y* axis be increasing from top to bottom?
        If ``None``, use the default for the Matplotlib function.
    add_colorbar : bool, optional
        Add colorbar to axes.
    add_labels : bool, optional
        Use xarray metadata to label axes.
    vmin : float or None, optional
        Lower value to anchor the colormap, otherwise it is inferred from the
        data and other keyword arguments. When a diverging dataset is inferred,
        setting `vmin` or `vmax` will fix the other by symmetry around
        ``center``. Setting both values prevents use of a diverging colormap.
        If discrete levels are provided as an explicit list, both of these
        values are ignored.
    vmax : float or None, optional
        Upper value to anchor the colormap, otherwise it is inferred from the
        data and other keyword arguments. When a diverging dataset is inferred,
        setting `vmin` or `vmax` will fix the other by symmetry around
        ``center``. Setting both values prevents use of a diverging colormap.
        If discrete levels are provided as an explicit list, both of these
        values are ignored.
    cmap : matplotlib colormap name or colormap, optional
        The mapping from data values to color space. If not provided, this
        will be either be ``'viridis'`` (if the function infers a sequential
        dataset) or ``'RdBu_r'`` (if the function infers a diverging dataset).
        See :doc:`Choosing Colormaps in Matplotlib <matplotlib:tutorials/colors/colormaps>`
        for more information.

        If *seaborn* is installed, ``cmap`` may also be a
        `seaborn color palette <https://seaborn.pydata.org/tutorial/color_palettes.html>`_.
        Note: if ``cmap`` is a seaborn color palette and the plot type
        is not ``'contour'`` or ``'contourf'``, ``levels`` must also be specified.
    center : float, optional
        The value at which to center the colormap. Passing this value implies
        use of a diverging colormap. Setting it to ``False`` prevents use of a
        diverging colormap.
    robust : bool, optional
        If ``True`` and ``vmin`` or ``vmax`` are absent, the colormap range is
        computed with 2nd and 98th percentiles instead of the extreme values.
    extend : {'neither', 'both', 'min', 'max'}, optional
        How to draw arrows extending the colorbar beyond its limits. If not
        provided, ``extend`` is inferred from ``vmin``, ``vmax`` and the data limits.
    levels : int or array-like, optional
        Split the colormap (``cmap``) into discrete color intervals. If an integer
        is provided, "nice" levels are chosen based on the data range: this can
        imply that the final number of levels is not exactly the expected one.
        Setting ``vmin`` and/or ``vmax`` with ``levels=N`` is equivalent to
        setting ``levels=np.linspace(vmin, vmax, N)``.
    infer_intervals : bool, optional
        Only applies to pcolormesh. If ``True``, the coordinate intervals are
        passed to pcolormesh. If ``False``, the original coordinates are used
        (this can be useful for certain map projections). The default is to
        always infer intervals, unless the mesh is irregular and plotted on
        a map projection.
    colors : str or array-like of color-like, optional
        A single color or a sequence of colors. If the plot type is not ``'contour'``
        or ``'contourf'``, the ``levels`` argument is required.
    subplot_kws : dict, optional
        Dictionary of keyword arguments for Matplotlib subplots. Only used
        for 2D and faceted plots.
        (see :py:meth:`matplotlib:matplotlib.figure.Figure.add_subplot`).
    cbar_ax : matplotlib axes object, optional
        Axes in which to draw the colorbar.
    cbar_kwargs : dict, optional
        Dictionary of keyword arguments to pass to the colorbar
        (see :meth:`matplotlib:matplotlib.figure.Figure.colorbar`).
    xscale : {'linear', 'symlog', 'log', 'logit'} or None, optional
        Specifies scaling for the x-axes.
    yscale : {'linear', 'symlog', 'log', 'logit'} or None, optional
        Specifies scaling for the y-axes.
    xticks : ArrayLike or None, optional
        Specify tick locations for x-axes.
    yticks : ArrayLike or None, optional
        Specify tick locations for y-axes.
    xlim : ArrayLike or None, optional
        Specify x-axes limits.
    ylim : ArrayLike or None, optional
        Specify y-axes limits.
    norm : matplotlib.colors.Normalize, optional
        If ``norm`` has ``vmin`` or ``vmax`` specified, the corresponding
        kwarg must be ``None``.
    **kwargs : optional
        Additional keyword arguments to wrapped Matplotlib function.

    Returns
    -------
    artist :
        The same type of primitive artist that the wrapped Matplotlib
        function returns.
    """

    # Build on the original docstring
    plotfunc.__doc__ = f"{plotfunc.__doc__}\n{commondoc}"

    @functools.wraps(
        plotfunc, assigned=("__module__", "__name__", "__qualname__", "__doc__")
    )
    def newplotfunc(
        darray: DataArray,
        *args: Any,
        x: Hashable | None = None,
        y: Hashable | None = None,
        figsize: Iterable[float] | None = None,
        size: float | None = None,
        aspect: float | None = None,
        ax: Axes | None = None,
        row: Hashable | None = None,
        col: Hashable | None = None,
        col_wrap: int | None = None,
        xincrease: bool | None = True,
        yincrease: bool | None = True,
        add_colorbar: bool | None = None,
        add_labels: bool = True,
        vmin: float | None = None,
        vmax: float | None = None,
        cmap: str | Colormap | None = None,
        center: float | None = None,
        robust: bool = False,
        extend: ExtendOptions = None,
        levels: ArrayLike | None = None,
        infer_intervals=None,
        colors: str | ArrayLike | None = None,
        subplot_kws: dict[str, Any] | None = None,
        cbar_ax: Axes | None = None,
        cbar_kwargs: dict[str, Any] | None = None,
        xscale: ScaleOptions = None,
        yscale: ScaleOptions = None,
        xticks: ArrayLike | None = None,
        yticks: ArrayLike | None = None,
        xlim: ArrayLike | None = None,
        ylim: ArrayLike | None = None,
        norm: Normalize | None = None,
        **kwargs: Any,
    ) -> Any:
        # All 2d plots in xarray share this function signature.

        if args:
            # TODO: Deprecated since 2022.10:
            msg = "Using positional arguments is deprecated for plot methods, use keyword arguments instead."
            assert x is None
            x = args[0]
            if len(args) > 1:
                assert y is None
                y = args[1]
            if len(args) > 2:
                raise ValueError(msg)
            else:
                warnings.warn(msg, DeprecationWarning, stacklevel=2)
        del args

        # Decide on a default for the colorbar before facetgrids
        if add_colorbar is None:
            add_colorbar = True
            if plotfunc.__name__ == "contour" or (
                plotfunc.__name__ == "surface" and cmap is None
            ):
                add_colorbar = False
        imshow_rgb = plotfunc.__name__ == "imshow" and darray.ndim == (
            3 + (row is not None) + (col is not None)
        )
        if imshow_rgb:
            # Don't add a colorbar when showing an image with explicit colors
            add_colorbar = False
            # Matplotlib does not support normalising RGB data, so do it here.
            # See eg. https://github.com/matplotlib/matplotlib/pull/10220
            if robust or vmax is not None or vmin is not None:
                darray = _rescale_imshow_rgb(darray.as_numpy(), vmin, vmax, robust)
                vmin, vmax, robust = None, None, False

        if subplot_kws is None:
            subplot_kws = dict()

        if plotfunc.__name__ == "surface" and not kwargs.get("_is_facetgrid", False):
            if ax is None:
                # TODO: Importing Axes3D is no longer necessary in matplotlib >= 3.2.
                # Remove when minimum requirement of matplotlib is 3.2:
                from mpl_toolkits.mplot3d import Axes3D  # noqa: F401

                # delete so it does not end up in locals()
                del Axes3D

                # Need to create a "3d" Axes instance for surface plots
                subplot_kws["projection"] = "3d"

            # In facet grids, shared axis labels don't make sense for surface plots
            sharex = False
            sharey = False

        # Handle facetgrids first
        if row or col:
            allargs = locals().copy()
            del allargs["darray"]
            del allargs["imshow_rgb"]
            allargs.update(allargs.pop("kwargs"))
            # Need the decorated plotting function
            allargs["plotfunc"] = globals()[plotfunc.__name__]
            return _easy_facetgrid(darray, kind="dataarray", **allargs)

        if darray.ndim == 0 or darray.size == 0:
            # TypeError to be consistent with pandas
            raise TypeError("No numeric data to plot.")

        plt = import_matplotlib_pyplot()

        if (
            plotfunc.__name__ == "surface"
            and not kwargs.get("_is_facetgrid", False)
            and ax is not None
        ):
            import mpl_toolkits

            if not isinstance(ax, mpl_toolkits.mplot3d.Axes3D):
                raise ValueError(
                    "If ax is passed to surface(), it must be created with "
                    'projection="3d"'
                )

        rgb = kwargs.pop("rgb", None)
        if rgb is not None and plotfunc.__name__ != "imshow":
            raise ValueError('The "rgb" keyword is only valid for imshow()')
        elif rgb is not None and not imshow_rgb:
            raise ValueError(
                'The "rgb" keyword is only valid for imshow()'
                "with a three-dimensional array (per facet)"
            )

        xlab, ylab = _infer_xy_labels(
            darray=darray, x=x, y=y, imshow=imshow_rgb, rgb=rgb
        )

        xval = darray[xlab]
        yval = darray[ylab]

        if xval.ndim > 1 or yval.ndim > 1 or plotfunc.__name__ == "surface":
            # Passing 2d coordinate values, need to ensure they are transposed the same
            # way as darray.
            # Also surface plots always need 2d coordinates
            xval = xval.broadcast_like(darray)
            yval = yval.broadcast_like(darray)
            dims = darray.dims
        else:
            dims = (yval.dims[0], xval.dims[0])

        # May need to transpose for correct x, y labels
        # xlab may be the name of a coord, we have to check for dim names
        if imshow_rgb:
            # For RGB[A] images, matplotlib requires the color dimension
            # to be last.  In Xarray the order should be unimportant, so
            # we transpose to (y, x, color) to make this work.
            yx_dims = (ylab, xlab)
            dims = yx_dims + tuple(d for d in darray.dims if d not in yx_dims)

        if dims != darray.dims:
            darray = darray.transpose(*dims, transpose_coords=True)

        # better to pass the ndarrays directly to plotting functions
        xvalnp = xval.to_numpy()
        yvalnp = yval.to_numpy()

        # Pass the data as a masked ndarray too
        zval = darray.to_masked_array(copy=False)

        # Replace pd.Intervals if contained in xval or yval.
        xplt, xlab_extra = _resolve_intervals_2dplot(xvalnp, plotfunc.__name__)
        yplt, ylab_extra = _resolve_intervals_2dplot(yvalnp, plotfunc.__name__)

        _ensure_plottable(xplt, yplt, zval)

        cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs(
            plotfunc,
            zval.data,
            **locals(),
            _is_facetgrid=kwargs.pop("_is_facetgrid", False),
        )

        if "contour" in plotfunc.__name__:
            # extend is a keyword argument only for contour and contourf, but
            # passing it to the colorbar is sufficient for imshow and
            # pcolormesh
            kwargs["extend"] = cmap_params["extend"]
            kwargs["levels"] = cmap_params["levels"]
            # if colors == a single color, matplotlib draws dashed negative
            # contours. we lose this feature if we pass cmap and not colors
            if isinstance(colors, str):
                cmap_params["cmap"] = None
                kwargs["colors"] = colors

        if "pcolormesh" == plotfunc.__name__:
            kwargs["infer_intervals"] = infer_intervals
            kwargs["xscale"] = xscale
            kwargs["yscale"] = yscale

        if "imshow" == plotfunc.__name__ and isinstance(aspect, str):
            # forbid usage of mpl strings
            raise ValueError("plt.imshow's `aspect` kwarg is not available in xarray")

        ax = get_axis(figsize, size, aspect, ax, **subplot_kws)

        primitive = plotfunc(
            xplt,
            yplt,
            zval,
            ax=ax,
            cmap=cmap_params["cmap"],
            vmin=cmap_params["vmin"],
            vmax=cmap_params["vmax"],
            norm=cmap_params["norm"],
            **kwargs,
        )

        # Label the plot with metadata
        if add_labels:
            ax.set_xlabel(label_from_attrs(darray[xlab], xlab_extra))
            ax.set_ylabel(label_from_attrs(darray[ylab], ylab_extra))
            ax.set_title(darray._title_for_slice())
            if plotfunc.__name__ == "surface":
                ax.set_zlabel(label_from_attrs(darray))

        if add_colorbar:
            if add_labels and "label" not in cbar_kwargs:
                cbar_kwargs["label"] = label_from_attrs(darray)
            cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params)
        elif cbar_ax is not None or cbar_kwargs:
            # inform the user about keywords which aren't used
            raise ValueError(
                "cbar_ax and cbar_kwargs can't be used with add_colorbar=False."
            )

        # origin kwarg overrides yincrease
        if "origin" in kwargs:
            yincrease = None

        _update_axes(
            ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim
        )

        # Rotate dates on xlabels
        # Do this without calling autofmt_xdate so that x-axes ticks
        # on other subplots (if any) are not deleted.
        # https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
        if np.issubdtype(xplt.dtype, np.datetime64):
            for xlabels in ax.get_xticklabels():
                xlabels.set_rotation(30)
                xlabels.set_horizontalalignment("right")

        return primitive

    # we want to actually expose the signature of newplotfunc
    # and not the copied **kwargs from the plotfunc which
    # functools.wraps adds, so delete the wrapped attr
    del newplotfunc.__wrapped__

    return newplotfunc


@overload
def imshow(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: float | None = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> AxesImage:
    ...


@overload
def imshow(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def imshow(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@_plot2d
def imshow(
    x: np.ndarray, y: np.ndarray, z: np.ma.core.MaskedArray, ax: Axes, **kwargs: Any
) -> AxesImage:
    """
    Image plot of 2D DataArray.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.imshow`.

    While other plot methods require the DataArray to be strictly
    two-dimensional, ``imshow`` also accepts a 3D array where some
    dimension can be interpreted as RGB or RGBA color channels and
    allows this dimension to be specified via the kwarg ``rgb=``.

    Unlike :py:func:`matplotlib:matplotlib.pyplot.imshow`, which ignores ``vmin``/``vmax``
    for RGB(A) data,
    xarray *will* use ``vmin`` and ``vmax`` for RGB(A) data
    by applying a single scaling factor and offset to all bands.
    Passing  ``robust=True`` infers ``vmin`` and ``vmax``
    :ref:`in the usual way <robust-plotting>`.
    Additionally the y-axis is not inverted by default, you can
    restore the matplotlib behavior by setting `yincrease=False`.

    .. note::
        This function needs uniformly spaced coordinates to
        properly label the axes. Call :py:meth:`DataArray.plot` to check.

    The pixels are centered on the coordinates. For example, if the coordinate
    value is 3.2, then the pixels for those coordinates will be centered on 3.2.
    """

    if x.ndim != 1 or y.ndim != 1:
        raise ValueError(
            "imshow requires 1D coordinates, try using pcolormesh or contour(f)"
        )

    def _center_pixels(x):
        """Center the pixels on the coordinates."""
        if np.issubdtype(x.dtype, str):
            # When using strings as inputs imshow converts it to
            # integers. Choose extent values which puts the indices in
            # in the center of the pixels:
            return 0 - 0.5, len(x) - 0.5

        try:
            # Center the pixels assuming uniform spacing:
            xstep = 0.5 * (x[1] - x[0])
        except IndexError:
            # Arbitrary default value, similar to matplotlib behaviour:
            xstep = 0.1

        return x[0] - xstep, x[-1] + xstep

    # Center the pixels:
    left, right = _center_pixels(x)
    top, bottom = _center_pixels(y)

    defaults: dict[str, Any] = {"origin": "upper", "interpolation": "nearest"}

    if not hasattr(ax, "projection"):
        # not for cartopy geoaxes
        defaults["aspect"] = "auto"

    # Allow user to override these defaults
    defaults.update(kwargs)

    if defaults["origin"] == "upper":
        defaults["extent"] = [left, right, bottom, top]
    else:
        defaults["extent"] = [left, right, top, bottom]

    if z.ndim == 3:
        # matplotlib imshow uses black for missing data, but Xarray makes
        # missing data transparent.  We therefore add an alpha channel if
        # there isn't one, and set it to transparent where data is masked.
        if z.shape[-1] == 3:
            alpha = np.ma.ones(z.shape[:2] + (1,), dtype=z.dtype)
            if np.issubdtype(z.dtype, np.integer):
                alpha *= 255
            z = np.ma.concatenate((z, alpha), axis=2)
        else:
            z = z.copy()
        z[np.any(z.mask, axis=-1), -1] = 0

    primitive = ax.imshow(z, **defaults)

    # If x or y are strings the ticklabels have been replaced with
    # integer indices. Replace them back to strings:
    for axis, v in [("x", x), ("y", y)]:
        if np.issubdtype(v.dtype, str):
            getattr(ax, f"set_{axis}ticks")(np.arange(len(v)))
            getattr(ax, f"set_{axis}ticklabels")(v)

    return primitive


@overload
def contour(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: float | None = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> QuadContourSet:
    ...


@overload
def contour(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def contour(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@_plot2d
def contour(
    x: np.ndarray, y: np.ndarray, z: np.ndarray, ax: Axes, **kwargs: Any
) -> QuadContourSet:
    """
    Contour plot of 2D DataArray.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.contour`.
    """
    primitive = ax.contour(x, y, z, **kwargs)
    return primitive


@overload
def contourf(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> QuadContourSet:
    ...


@overload
def contourf(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def contourf(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@_plot2d
def contourf(
    x: np.ndarray, y: np.ndarray, z: np.ndarray, ax: Axes, **kwargs: Any
) -> QuadContourSet:
    """
    Filled contour plot of 2D DataArray.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.contourf`.
    """
    primitive = ax.contourf(x, y, z, **kwargs)
    return primitive


@overload
def pcolormesh(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> QuadMesh:
    ...


@overload
def pcolormesh(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def pcolormesh(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@_plot2d
def pcolormesh(
    x: np.ndarray,
    y: np.ndarray,
    z: np.ndarray,
    ax: Axes,
    xscale: ScaleOptions | None = None,
    yscale: ScaleOptions | None = None,
    infer_intervals=None,
    **kwargs: Any,
) -> QuadMesh:
    """
    Pseudocolor plot of 2D DataArray.

    Wraps :py:func:`matplotlib:matplotlib.pyplot.pcolormesh`.
    """

    # decide on a default for infer_intervals (GH781)
    x = np.asarray(x)
    if infer_intervals is None:
        if hasattr(ax, "projection"):
            if len(x.shape) == 1:
                infer_intervals = True
            else:
                infer_intervals = False
        else:
            infer_intervals = True

    if any(np.issubdtype(k.dtype, str) for k in (x, y)):
        # do not infer intervals if any axis contains str ticks, see #6775
        infer_intervals = False

    if infer_intervals and (
        (np.shape(x)[0] == np.shape(z)[1])
        or ((x.ndim > 1) and (np.shape(x)[1] == np.shape(z)[1]))
    ):
        if x.ndim == 1:
            x = _infer_interval_breaks(x, check_monotonic=True, scale=xscale)
        else:
            # we have to infer the intervals on both axes
            x = _infer_interval_breaks(x, axis=1, scale=xscale)
            x = _infer_interval_breaks(x, axis=0, scale=xscale)

    if infer_intervals and (np.shape(y)[0] == np.shape(z)[0]):
        if y.ndim == 1:
            y = _infer_interval_breaks(y, check_monotonic=True, scale=yscale)
        else:
            # we have to infer the intervals on both axes
            y = _infer_interval_breaks(y, axis=1, scale=yscale)
            y = _infer_interval_breaks(y, axis=0, scale=yscale)

    ax.grid(False)
    primitive = ax.pcolormesh(x, y, z, **kwargs)

    # by default, pcolormesh picks "round" values for bounds
    # this results in ugly looking plots with lots of surrounding whitespace
    if not hasattr(ax, "projection") and x.ndim == 1 and y.ndim == 1:
        # not a cartopy geoaxis
        ax.set_xlim(x[0], x[-1])
        ax.set_ylim(y[0], y[-1])

    return primitive


@overload
def surface(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: None = None,  # no wrap -> primitive
    col: None = None,  # no wrap -> primitive
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> Poly3DCollection:
    ...


@overload
def surface(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable | None = None,
    col: Hashable,  # wrap -> FacetGrid
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@overload
def surface(
    darray: DataArray,
    x: Hashable | None = None,
    y: Hashable | None = None,
    *,
    figsize: Iterable[float] | None = None,
    size: float | None = None,
    aspect: AspectOptions = None,
    ax: Axes | None = None,
    row: Hashable,  # wrap -> FacetGrid
    col: Hashable | None = None,
    col_wrap: int | None = None,
    xincrease: bool | None = True,
    yincrease: bool | None = True,
    add_colorbar: bool | None = None,
    add_labels: bool = True,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap: str | Colormap | None = None,
    center: float | None = None,
    robust: bool = False,
    extend: ExtendOptions = None,
    levels: ArrayLike | None = None,
    infer_intervals=None,
    colors: str | ArrayLike | None = None,
    subplot_kws: dict[str, Any] | None = None,
    cbar_ax: Axes | None = None,
    cbar_kwargs: dict[str, Any] | None = None,
    xscale: ScaleOptions = None,
    yscale: ScaleOptions = None,
    xticks: ArrayLike | None = None,
    yticks: ArrayLike | None = None,
    xlim: ArrayLike | None = None,
    ylim: ArrayLike | None = None,
    norm: Normalize | None = None,
    **kwargs: Any,
) -> FacetGrid[DataArray]:
    ...


@_plot2d
def surface(
    x: np.ndarray, y: np.ndarray, z: np.ndarray, ax: Axes, **kwargs: Any
) -> Poly3DCollection:
    """
    Surface plot of 2D DataArray.

    Wraps :py:meth:`matplotlib:mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface`.
    """
    primitive = ax.plot_surface(x, y, z, **kwargs)
    return primitive
