"""
Colorimetry Plotting
====================

Define the colorimetry plotting objects:

-   :func:`colour.plotting.plot_single_sd`
-   :func:`colour.plotting.plot_multi_sds`
-   :func:`colour.plotting.plot_single_cmfs`
-   :func:`colour.plotting.plot_multi_cmfs`
-   :func:`colour.plotting.plot_single_illuminant_sd`
-   :func:`colour.plotting.plot_multi_illuminant_sds`
-   :func:`colour.plotting.plot_visible_spectrum`
-   :func:`colour.plotting.plot_single_lightness_function`
-   :func:`colour.plotting.plot_multi_lightness_functions`
-   :func:`colour.plotting.plot_single_luminance_function`
-   :func:`colour.plotting.plot_multi_luminance_functions`
-   :func:`colour.plotting.plot_blackbody_spectral_radiance`
-   :func:`colour.plotting.plot_blackbody_colours`

References
----------
-   :cite:`Spiker2015a` : Borer, T. (2017). Private Discussion with Mansencal,
    T. and Shaw, N.
"""

from __future__ import annotations

import typing
from functools import reduce

import matplotlib.pyplot as plt
import numpy as np

if typing.TYPE_CHECKING:
    from collections.abc import ValuesView

from matplotlib.patches import Polygon

if typing.TYPE_CHECKING:
    from matplotlib.figure import Figure
    from matplotlib.axes import Axes

from colour.algebra import LinearInterpolator, normalise_maximum, sdiv, sdiv_mode
from colour.colorimetry import (
    CCS_ILLUMINANTS,
    LIGHTNESS_METHODS,
    LUMINANCE_METHODS,
    SDS_ILLUMINANTS,
    MultiSpectralDistributions,
    SpectralDistribution,
    SpectralShape,
    sd_blackbody,
    sd_ones,
    sd_to_XYZ,
    sds_and_msds_to_sds,
    wavelength_to_XYZ,
)

if typing.TYPE_CHECKING:
    from colour.hints import (
        Any,
        Callable,
        Dict,
        Sequence,
        Tuple,
    )

from colour.hints import List, cast
from colour.plotting import (
    CONSTANTS_COLOUR_STYLE,
    XYZ_to_plotting_colourspace,
    artist,
    filter_cmfs,
    filter_illuminants,
    filter_passthrough,
    override_style,
    plot_multi_functions,
    plot_single_colour_swatch,
    render,
    update_settings_collection,
)
from colour.utilities import (
    as_float_array,
    domain_range_scale,
    first_item,
    ones,
    optional,
    tstack,
)

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
    "plot_single_sd",
    "plot_multi_sds",
    "plot_single_cmfs",
    "plot_multi_cmfs",
    "plot_single_illuminant_sd",
    "plot_multi_illuminant_sds",
    "plot_visible_spectrum",
    "plot_single_lightness_function",
    "plot_multi_lightness_functions",
    "plot_single_luminance_function",
    "plot_multi_luminance_functions",
    "plot_blackbody_spectral_radiance",
    "plot_blackbody_colours",
]


@override_style()
def plot_single_sd(
    sd: SpectralDistribution,
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    out_of_gamut_clipping: bool = True,
    modulate_colours_with_sd_amplitude: bool = False,
    equalize_sd_amplitude: bool = False,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified spectral distribution.

    Parameters
    ----------
    sd
        Spectral distribution to plot.
    cmfs
        Standard observer colour matching functions used for computing the
        spectrum domain and colours. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.
    out_of_gamut_clipping
        Whether to clip out of gamut colours. Otherwise, the colours will
        be offset by the absolute minimal colour, resulting in rendering
        on a gray background that is less saturated and smoother.
    modulate_colours_with_sd_amplitude
        Whether to modulate the colours with the spectral distribution
        amplitude.
    equalize_sd_amplitude
        Whether to equalize the spectral distribution amplitude.
        Equalization occurs after the colours modulation; thus, setting
        both arguments to *True* will generate a spectrum strip where each
        wavelength colour is modulated by the spectral distribution
        amplitude. The usual 5% margin above the spectral distribution is
        also omitted.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    References
    ----------
    :cite:`Spiker2015a`

    Examples
    --------
    >>> from colour import SpectralDistribution
    >>> data = {
    ...     500: 0.0651,
    ...     520: 0.0705,
    ...     540: 0.0772,
    ...     560: 0.0870,
    ...     580: 0.1128,
    ...     600: 0.1360,
    ... }
    >>> sd = SpectralDistribution(data, name="Custom")
    >>> plot_single_sd(sd)  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Single_SD.png
        :align: center
        :alt: plot_single_sd
    """

    _figure, axes = artist(**kwargs)

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    sd = sd.copy()
    sd.interpolator = LinearInterpolator
    wavelengths = cmfs.wavelengths[
        np.logical_and(
            cmfs.wavelengths >= max(min(cmfs.wavelengths), min(sd.wavelengths)),
            cmfs.wavelengths <= min(max(cmfs.wavelengths), max(sd.wavelengths)),
        )
    ]
    values = sd[wavelengths]

    RGB = XYZ_to_plotting_colourspace(
        wavelength_to_XYZ(wavelengths, cmfs),
        CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["E"],
        apply_cctf_encoding=False,
    )

    if not out_of_gamut_clipping:
        RGB += np.abs(np.min(RGB))

    RGB = normalise_maximum(RGB)

    if modulate_colours_with_sd_amplitude:
        with sdiv_mode():
            RGB *= sdiv(values, np.max(values))[..., None]

    RGB = CONSTANTS_COLOUR_STYLE.colour.colourspace.cctf_encoding(RGB)

    if equalize_sd_amplitude:
        values = ones(values.shape)

    margin = 0 if equalize_sd_amplitude else 0.05

    x_min, x_max = min(wavelengths), max(wavelengths)
    y_min, y_max = 0, max(values) + max(values) * margin

    polygon = Polygon(
        np.vstack(
            [
                (x_min, 0),
                tstack([wavelengths, values]),
                (x_max, 0),
            ]
        ),
        facecolor="none",
        edgecolor="none",
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
    )
    axes.add_patch(polygon)

    axes.bar(
        x=wavelengths,
        height=max(values),
        width=np.min(np.diff(wavelengths)) if len(wavelengths) > 1 else 1,
        color=RGB,
        align="edge",
        clip_path=polygon,
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
    )

    axes.plot(
        wavelengths,
        values,
        color=CONSTANTS_COLOUR_STYLE.colour.dark,
        zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
    )

    settings: Dict[str, Any] = {
        "axes": axes,
        "bounding_box": (x_min, x_max, y_min, y_max),
        "title": f"{sd.display_name} - {cmfs.display_name}",
        "x_label": "Wavelength $\\lambda$ (nm)",
        "y_label": "Spectral Distribution",
    }
    settings.update(kwargs)

    return render(**settings)


@override_style()
def plot_multi_sds(
    sds: (
        Sequence[SpectralDistribution | MultiSpectralDistributions]
        | SpectralDistribution
        | MultiSpectralDistributions
        | ValuesView
    ),
    plot_kwargs: dict | List[dict] | None = None,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot specified spectral distributions.

    Parameters
    ----------
    sds
        Spectral distributions or multi-spectral distributions to plot.
        ``sds`` can be a single
        :class:`colour.MultiSpectralDistributions` class instance, a list
        of :class:`colour.MultiSpectralDistributions` class instances or
        a list of :class:`colour.SpectralDistribution` class instances.
    plot_kwargs
        Keyword arguments for the :func:`matplotlib.pyplot.plot`
        definition, used to control the style of the plotted spectral
        distributions. ``plot_kwargs`` can be either a single dictionary
        applied to all the plotted spectral distributions with the same
        settings or a sequence of dictionaries with different settings
        for each plotted spectral distribution. The following special
        keyword arguments can also be used:

        -   ``illuminant`` : The illuminant used to compute the spectral
            distributions colours. The default is the illuminant
            associated with the whitepoint of the default plotting
            colourspace. ``illuminant`` can be of any type or form
            supported by the :func:`colour.plotting.common.filter_cmfs`
            definition.
        -   ``cmfs`` : The standard observer colour matching functions
            used for computing the spectral distributions colours.
            ``cmfs`` can be of any type or form supported by the
            :func:`colour.plotting.common.filter_cmfs` definition.
        -   ``normalise_sd_colours`` : Whether to normalise the computed
            spectral distributions colours. The default is *True*.
        -   ``use_sd_colours`` : Whether to use the computed spectral
            distributions colours under the plotting colourspace
            illuminant. Alternatively, it is possible to use the
            :func:`matplotlib.pyplot.plot` definition ``color`` argument
            with pre-computed values. The default is *True*.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> from colour import SpectralDistribution
    >>> data_1 = {
    ...     500: 0.004900,
    ...     510: 0.009300,
    ...     520: 0.063270,
    ...     530: 0.165500,
    ...     540: 0.290400,
    ...     550: 0.433450,
    ...     560: 0.594500,
    ... }
    >>> data_2 = {
    ...     500: 0.323000,
    ...     510: 0.503000,
    ...     520: 0.710000,
    ...     530: 0.862000,
    ...     540: 0.954000,
    ...     550: 0.994950,
    ...     560: 0.995000,
    ... }
    >>> sd_1 = SpectralDistribution(data_1, name="Custom 1")
    >>> sd_2 = SpectralDistribution(data_2, name="Custom 2")
    >>> plot_kwargs = [
    ...     {"use_sd_colours": True},
    ...     {"use_sd_colours": True, "linestyle": "dashed"},
    ... ]
    >>> plot_multi_sds([sd_1, sd_2], plot_kwargs=plot_kwargs)
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Multi_SDS.png
        :align: center
        :alt: plot_multi_sds
    """

    _figure, axes = artist(**kwargs)

    sds_converted = sds_and_msds_to_sds(sds)

    plot_settings_collection = [
        {
            "label": f"{sd.display_name}",
            "zorder": CONSTANTS_COLOUR_STYLE.zorder.midground_line,
            "cmfs": "CIE 1931 2 Degree Standard Observer",
            "illuminant": SDS_ILLUMINANTS["E"],
            "use_sd_colours": False,
            "normalise_sd_colours": False,
        }
        for sd in sds_converted
    ]

    if plot_kwargs is not None:
        update_settings_collection(
            plot_settings_collection, plot_kwargs, len(sds_converted)
        )

    x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
    for i, sd in enumerate(sds_converted):
        plot_settings = plot_settings_collection[i]

        cmfs = cast(
            "MultiSpectralDistributions",
            first_item(filter_cmfs(plot_settings.pop("cmfs")).values()),
        )
        illuminant = cast(
            "SpectralDistribution",
            first_item(filter_illuminants(plot_settings.pop("illuminant")).values()),
        )
        normalise_sd_colours = plot_settings.pop("normalise_sd_colours")
        use_sd_colours = plot_settings.pop("use_sd_colours")

        wavelengths, values = sd.wavelengths, sd.values

        shape = sd.shape
        x_limit_min.append(shape.start)
        x_limit_max.append(shape.end)
        y_limit_min.append(min(values))
        y_limit_max.append(max(values))

        if use_sd_colours:
            with domain_range_scale("1"):
                XYZ = sd_to_XYZ(sd, cmfs, illuminant)

            if normalise_sd_colours:
                XYZ /= XYZ[..., 1]

            plot_settings["color"] = np.clip(XYZ_to_plotting_colourspace(XYZ), 0, 1)

        axes.plot(wavelengths, values, **plot_settings)

    bounding_box = (
        min(x_limit_min),
        max(x_limit_max),
        min(y_limit_min),
        max(y_limit_max) * 1.05,
    )
    settings: Dict[str, Any] = {
        "axes": axes,
        "bounding_box": bounding_box,
        "legend": True,
        "x_label": "Wavelength $\\lambda$ (nm)",
        "y_label": "Spectral Distribution",
    }
    settings.update(kwargs)

    return render(**settings)


@override_style()
def plot_single_cmfs(
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot specified colour matching functions.

    Parameters
    ----------
    cmfs
        Colour matching functions to plot. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_cmfs`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_single_cmfs("CIE 1931 2 Degree Standard Observer")
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Single_CMFS.png
        :align: center
        :alt: plot_single_cmfs
    """

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    settings: Dict[str, Any] = {
        "title": f"{cmfs.display_name} - Colour Matching Functions"
    }
    settings.update(kwargs)

    return plot_multi_cmfs((cmfs,), **settings)


@override_style()
def plot_multi_cmfs(
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ),
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified colour matching functions.

    Parameters
    ----------
    cmfs
        Colour matching functions to plot. ``cmfs`` elements can be of any
        type or form supported by the
        :func:`colour.plotting.common.filter_cmfs` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> cmfs = [
    ...     "CIE 1931 2 Degree Standard Observer",
    ...     "CIE 1964 10 Degree Standard Observer",
    ... ]
    >>> plot_multi_cmfs(cmfs)  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Multi_CMFS.png
        :align: center
        :alt: plot_multi_cmfs
    """

    cmfs = cast("List[MultiSpectralDistributions]", list(filter_cmfs(cmfs).values()))  # pyright: ignore

    _figure, axes = artist(**kwargs)

    axes.axhline(
        color=CONSTANTS_COLOUR_STYLE.colour.dark,
        linestyle="--",
        zorder=CONSTANTS_COLOUR_STYLE.zorder.foreground_line,
    )

    x_limit_min, x_limit_max, y_limit_min, y_limit_max = [], [], [], []
    for i, cmfs_i in enumerate(cmfs):
        for j, RGB in enumerate(as_float_array([[1, 0, 0], [0, 1, 0], [0, 0, 1]])):
            RGB = [  # noqa: PLW2901
                reduce(lambda y, _: y * 0.5, range(i), x) for x in RGB
            ]
            values = cmfs_i.values[:, j]

            shape = cmfs_i.shape
            x_limit_min.append(shape.start)
            x_limit_max.append(shape.end)
            y_limit_min.append(np.min(values))
            y_limit_max.append(np.max(values))

            axes.plot(
                cmfs_i.wavelengths,
                values,
                color=RGB,
                label=f"{cmfs_i.display_labels[j]} - {cmfs_i.display_name}",
                zorder=CONSTANTS_COLOUR_STYLE.zorder.midground_line,
            )

    bounding_box = (
        min(x_limit_min),
        max(x_limit_max),
        min(y_limit_min) - np.abs(np.min(y_limit_min)) * 0.05,
        max(y_limit_max) + np.abs(np.max(y_limit_max)) * 0.05,
    )
    cmfs_display_names = ", ".join([cmfs_i.display_name for cmfs_i in cmfs])
    title = f"{cmfs_display_names} - Colour Matching Functions"

    settings: Dict[str, Any] = {
        "axes": axes,
        "bounding_box": bounding_box,
        "legend": True,
        "title": title,
        "x_label": "Wavelength $\\lambda$ (nm)",
        "y_label": "Tristimulus Values",
    }
    settings.update(kwargs)

    return render(**settings)


@override_style()
def plot_single_illuminant_sd(
    illuminant: SpectralDistribution | str,
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified single illuminant spectral distribution.

    Parameters
    ----------
    illuminant
        Illuminant to plot. ``illuminant`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_illuminants`
        definition.
    cmfs
        Standard observer colour matching functions used for computing the
        spectrum domain and colours. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_single_sd`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    References
    ----------
    :cite:`Spiker2015a`

    Examples
    --------
    >>> plot_single_illuminant_sd("A")  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Single_Illuminant_SD.png
        :align: center
        :alt: plot_single_illuminant_sd
    """

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    title = f"Illuminant {illuminant} - {cmfs.display_name}"

    illuminant = cast(
        "SpectralDistribution",
        first_item(filter_illuminants(illuminant).values()),
    )

    settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}
    settings.update(kwargs)

    return plot_single_sd(illuminant, **settings)


@override_style()
def plot_multi_illuminant_sds(
    illuminants: (SpectralDistribution | str | Sequence[SpectralDistribution | str]),
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the spectral distributions of the specified illuminants.

    Parameters
    ----------
    illuminants
        Illuminants to plot. ``illuminants`` elements can be of any type
        or form supported by the
        :func:`colour.plotting.common.filter_illuminants` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_sds`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_multi_illuminant_sds(["A", "B", "C"])  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Multi_Illuminant_SDS.png
        :align: center
        :alt: plot_multi_illuminant_sds
    """

    if "plot_kwargs" not in kwargs:
        kwargs["plot_kwargs"] = {}

    SD_E = SDS_ILLUMINANTS["E"]
    if isinstance(kwargs["plot_kwargs"], dict):
        kwargs["plot_kwargs"]["illuminant"] = SD_E
    else:
        for i in range(len(kwargs["plot_kwargs"])):
            kwargs["plot_kwargs"][i]["illuminant"] = SD_E

    illuminants = cast(
        "List[SpectralDistribution]",
        list(filter_illuminants(illuminants).values()),
    )  # pyright: ignore

    illuminant_display_names = ", ".join(
        [illuminant.display_name for illuminant in illuminants]
    )
    title = f"{illuminant_display_names} - Illuminants Spectral Distributions"

    settings: Dict[str, Any] = {"title": title, "y_label": "Relative Power"}
    settings.update(kwargs)

    return plot_multi_sds(illuminants, **settings)


@override_style(
    **{
        "ytick.left": False,
        "ytick.labelleft": False,
    }
)
def plot_visible_spectrum(
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    out_of_gamut_clipping: bool = True,
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the visible colour spectrum using the specified standard observer
    *CIE XYZ* colour matching functions.

    Parameters
    ----------
    cmfs
        Standard observer colour matching functions used for computing the
        spectrum domain and colours. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.
    out_of_gamut_clipping
        Whether to clip out of gamut colours. Otherwise, the colours will
        be offset by the absolute minimal colour, resulting in rendering
        on a gray background that is less saturated and smoother.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_single_sd`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    References
    ----------
    :cite:`Spiker2015a`

    Examples
    --------
    >>> plot_visible_spectrum()  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Visible_Spectrum.png
        :align: center
        :alt: plot_visible_spectrum
    """

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    bounding_box = (min(cmfs.wavelengths), max(cmfs.wavelengths), 0, 1)

    settings: Dict[str, Any] = {"bounding_box": bounding_box, "y_label": None}
    settings.update(kwargs)
    settings["show"] = False

    _figure, axes = plot_single_sd(
        sd_ones(cmfs.shape),
        cmfs=cmfs,
        out_of_gamut_clipping=out_of_gamut_clipping,
        **settings,
    )

    # Removing wavelength line as it doubles with the axes spine.
    axes.lines[0].remove()

    settings = {
        "axes": axes,
        "show": True,
        "title": f"The Visible Spectrum - {cmfs.display_name}",
        "x_label": "Wavelength $\\lambda$ (nm)",
    }
    settings.update(kwargs)

    return render(**settings)


@override_style()
def plot_single_lightness_function(
    function: Callable | str, **kwargs: Any
) -> Tuple[Figure, Axes]:
    """
    Plot the specified *Lightness* function.

    Parameters
    ----------
    function
        *Lightness* function to plot. ``function`` can be of any type or
        form supported by the
        :func:`colour.plotting.common.filter_passthrough` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_functions`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_single_lightness_function("CIE 1976")  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Single_Lightness_Function.png
        :align: center
        :alt: plot_single_lightness_function
    """

    settings: Dict[str, Any] = {"title": f"{function} - Lightness Function"}
    settings.update(kwargs)

    return plot_multi_lightness_functions((function,), **settings)


@override_style()
def plot_multi_lightness_functions(
    functions: Callable | str | Sequence[Callable | str],
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified *Lightness* functions.

    Parameters
    ----------
    functions
        *Lightness* functions to plot. ``functions`` elements can be of any
        type or form supported by the
        :func:`colour.plotting.common.filter_passthrough` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_functions`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_multi_lightness_functions(["CIE 1976", "Wyszecki 1963"])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Multi_Lightness_Functions.png
        :align: center
        :alt: plot_multi_lightness_functions
    """

    functions_filtered = filter_passthrough(LIGHTNESS_METHODS, functions)

    settings: Dict[str, Any] = {
        "bounding_box": (0, 1, 0, 1),
        "legend": True,
        "title": f"{', '.join(functions_filtered)} - Lightness Functions",
        "x_label": "Normalised Relative Luminance Y",
        "y_label": "Normalised Lightness",
    }
    settings.update(kwargs)

    with domain_range_scale("1"):
        return plot_multi_functions(functions_filtered, **settings)


@override_style()
def plot_single_luminance_function(
    function: Callable | str, **kwargs: Any
) -> Tuple[Figure, Axes]:
    """
    Plot the specified *Luminance* function.

    Parameters
    ----------
    function
        *Luminance* function to plot.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_functions`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_single_luminance_function("CIE 1976")  # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Single_Luminance_Function.png
        :align: center
        :alt: plot_single_luminance_function
    """

    settings: Dict[str, Any] = {"title": f"{function} - Luminance Function"}
    settings.update(kwargs)

    return plot_multi_luminance_functions((function,), **settings)


@override_style()
def plot_multi_luminance_functions(
    functions: Callable | str | Sequence[Callable | str],
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the specified *Luminance* functions.

    Parameters
    ----------
    functions
        *Luminance* functions to plot. ``functions`` elements can be of any
        type or form supported by the
        :func:`colour.plotting.common.filter_passthrough` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_multi_functions`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_multi_luminance_functions(["CIE 1976", "Newhall 1943"])
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Multi_Luminance_Functions.png
        :align: center
        :alt: plot_multi_luminance_functions
    """

    functions_filtered = filter_passthrough(LUMINANCE_METHODS, functions)

    settings: Dict[str, Any] = {
        "bounding_box": (0, 1, 0, 1),
        "legend": True,
        "title": f"{', '.join(functions_filtered)} - Luminance Functions",
        "x_label": "Normalised Munsell Value / Lightness",
        "y_label": "Normalised Relative Luminance Y",
    }
    settings.update(kwargs)

    with domain_range_scale("1"):
        return plot_multi_functions(functions_filtered, **settings)


@override_style()
def plot_blackbody_spectral_radiance(
    temperature: float = 3500,
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    blackbody: str = "VY Canis Major",
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot the spectral radiance of a blackbody at the specified temperature.

    Parameters
    ----------
    temperature
        Blackbody temperature.
    cmfs
        Standard observer colour matching functions used for computing the
        spectrum domain and colours. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.
    blackbody
        Blackbody name.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`,
        :func:`colour.plotting.plot_single_sd`,
        :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_blackbody_spectral_radiance(3500, blackbody="VY Canis Major")
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 2 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Blackbody_Spectral_Radiance.png
        :align: center
        :alt: plot_blackbody_spectral_radiance
    """

    figure = plt.figure()

    figure.subplots_adjust(hspace=CONSTANTS_COLOUR_STYLE.geometry.short / 2)

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    sd = sd_blackbody(temperature, cmfs.shape)

    axes = figure.add_subplot(211)
    settings: Dict[str, Any] = {
        "axes": axes,
        "title": f"{blackbody} - Spectral Radiance",
        "y_label": "W / (sr m$^2$) / m",
    }
    settings.update(kwargs)
    settings["show"] = False

    plot_single_sd(sd, cmfs.name, **settings)

    axes = figure.add_subplot(212)

    with domain_range_scale("1"):
        XYZ = sd_to_XYZ(sd, cmfs)

    RGB = normalise_maximum(XYZ_to_plotting_colourspace(XYZ))

    settings = {
        "axes": axes,
        "aspect": None,
        "title": f"{blackbody} - Colour",
        "x_label": f"{temperature}K",
        "y_label": "",
        "x_ticker": False,
        "y_ticker": False,
    }
    settings.update(kwargs)
    settings["show"] = False

    figure, axes = plot_single_colour_swatch(RGB, **settings)

    settings = {"axes": axes, "show": True}
    settings.update(kwargs)

    return render(**settings)


@override_style(
    **{
        "ytick.left": False,
        "ytick.labelleft": False,
    }
)
def plot_blackbody_colours(
    shape: SpectralShape | None = None,
    cmfs: (
        MultiSpectralDistributions | str | Sequence[MultiSpectralDistributions | str]
    ) = "CIE 1931 2 Degree Standard Observer",
    **kwargs: Any,
) -> Tuple[Figure, Axes]:
    """
    Plot blackbody colours across a temperature range.

    Parameters
    ----------
    shape
        Spectral shape defining the temperature range and sampling interval
        for the plot boundaries.
    cmfs
        Standard observer colour matching functions used for computing the
        spectrum domain and colours. ``cmfs`` can be of any type or form
        supported by the :func:`colour.plotting.common.filter_cmfs` definition.

    Other Parameters
    ----------------
    kwargs
        {:func:`colour.plotting.artist`, :func:`colour.plotting.render`},
        See the documentation of the previously listed definitions.

    Returns
    -------
    :class:`tuple`
        Current figure and axes.

    Examples
    --------
    >>> plot_blackbody_colours(SpectralShape(150, 12500, 50))
    ... # doctest: +ELLIPSIS
    (<Figure size ... with 1 Axes>, <...Axes...>)

    .. image:: ../_static/Plotting_Plot_Blackbody_Colours.png
        :align: center
        :alt: plot_blackbody_colours
    """

    shape = optional(shape, SpectralShape(150, 12500, 50))

    _figure, axes = artist(**kwargs)

    cmfs = cast("MultiSpectralDistributions", first_item(filter_cmfs(cmfs).values()))

    RGB = []
    temperatures = []

    for temperature in shape:
        sd = sd_blackbody(temperature, cmfs.shape)

        with domain_range_scale("1"):
            XYZ = sd_to_XYZ(sd, cmfs)

        RGB.append(normalise_maximum(XYZ_to_plotting_colourspace(XYZ)))
        temperatures.append(temperature)

    x_min, x_max = min(temperatures), max(temperatures)
    y_min, y_max = 0, 1

    padding = 0.1
    axes.bar(
        x=as_float_array(temperatures) - padding,
        height=1,
        width=shape.interval + (padding * shape.interval),
        color=RGB,
        align="edge",
        zorder=CONSTANTS_COLOUR_STYLE.zorder.background_polygon,
    )

    settings: Dict[str, Any] = {
        "axes": axes,
        "bounding_box": (x_min, x_max, y_min, y_max),
        "title": "Blackbody Colours",
        "x_label": "Temperature K",
        "y_label": None,
    }
    settings.update(kwargs)

    return render(**settings)
