import numpy as np
import pytest

import holoviews as hv
from holoviews.operation import operation
from holoviews.plotting.util import (
    _get_min_distance_numpy,
    bokeh_palette_to_palette,
    color_intervals,
    compute_overlayable_zorders,
    get_axis_padding,
    get_min_distance,
    get_range,
    initialize_dynamic,
    mplcmap_to_palette,
    process_cmap,
    split_dmap_overlay,
)
from holoviews.streams import PointerX


class TestOverlayableZorders:
    def test_compute_overlayable_zorders_holomap(self):
        hmap = hv.HoloMap({0: hv.Points([])})
        sources = compute_overlayable_zorders(hmap)
        assert sources[0] == [hmap, hmap.last]

    def test_compute_overlayable_zorders_with_overlaid_holomap(self):
        points = hv.Points([])
        hmap = hv.HoloMap({0: points})
        curve = hv.Curve([])
        combined = hmap * curve
        sources = compute_overlayable_zorders(combined)
        assert sources[0] == [points, combined.last, combined]

    def test_dynamic_compute_overlayable_zorders_two_mixed_layers(self):
        area = hv.Area(range(10))
        dmap = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        combined = area * dmap
        combined[()]
        sources = compute_overlayable_zorders(combined)
        assert sources[0] == [area]
        assert sources[1] == [dmap]

    def test_dynamic_compute_overlayable_zorders_two_mixed_layers_reverse(self):
        area = hv.Area(range(10))
        dmap = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        combined = dmap * area
        combined[()]
        sources = compute_overlayable_zorders(combined)
        assert sources[0] == [dmap]
        assert sources[1] == [area]

    def test_dynamic_compute_overlayable_zorders_two_dynamic_layers(self):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        dmap = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        combined = area * dmap
        combined[()]
        sources = compute_overlayable_zorders(combined)
        assert sources[0] == [area]
        assert sources[1] == [dmap]

    def test_dynamic_compute_overlayable_zorders_two_deep_dynamic_layers(self):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        area_redim = area.redim(x="x2")
        curve_redim = curve.redim(x="x2")
        combined = area_redim * curve_redim
        combined[()]
        sources = compute_overlayable_zorders(combined)
        assert area_redim in sources[0]
        assert area in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]
        assert curve_redim in sources[1]
        assert curve in sources[1]
        assert area_redim not in sources[1]
        assert area not in sources[1]

    def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers(self):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve2 = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        area_redim = area.redim(x="x2")
        curve_redim = curve.redim(x="x2")
        curve2_redim = curve2.redim(x="x3")
        combined = area_redim * curve_redim
        combined1 = combined * curve2_redim
        combined1[()]
        sources = compute_overlayable_zorders(combined1)
        assert area_redim in sources[0]
        assert area in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]
        assert curve2_redim not in sources[0]
        assert curve2 not in sources[0]

        assert curve_redim in sources[1]
        assert curve in sources[1]
        assert area_redim not in sources[1]
        assert area not in sources[1]
        assert curve2_redim not in sources[1]
        assert curve2 not in sources[1]

        assert curve2_redim in sources[2]
        assert curve2 in sources[2]
        assert area_redim not in sources[2]
        assert area not in sources[2]
        assert curve_redim not in sources[2]
        assert curve not in sources[2]

    def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_cloned(self):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve2 = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        area_redim = area.redim(x="x2")
        curve_redim = curve.redim(x="x2")
        curve2_redim = curve2.redim(x="x3")
        combined = area_redim * curve_redim
        combined1 = (combined * curve2_redim).redim(y="y2")
        combined1[()]
        sources = compute_overlayable_zorders(combined1)

        assert area_redim in sources[0]
        assert area in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]
        assert curve2_redim not in sources[0]
        assert curve2 not in sources[0]

        assert curve_redim in sources[1]
        assert curve in sources[1]
        assert area_redim not in sources[1]
        assert area not in sources[1]
        assert curve2_redim not in sources[1]
        assert curve2 not in sources[1]

        assert curve2_redim in sources[2]
        assert curve2 in sources[2]
        assert area_redim not in sources[2]
        assert area not in sources[2]
        assert curve_redim not in sources[2]
        assert curve not in sources[2]

    def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_overlays_reverse(
        self,
    ):
        area1 = hv.Area(range(10))
        area2 = hv.Area(range(10))
        overlay = area1 * area2
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve_redim = curve.redim(x="x2")
        combined = curve_redim * overlay
        combined[()]
        sources = compute_overlayable_zorders(combined)

        assert curve_redim in sources[0]
        assert curve in sources[0]
        assert overlay not in sources[0]

        assert area1 in sources[1]
        assert overlay in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

        assert area2 in sources[2]
        assert overlay in sources[2]
        assert curve_redim not in sources[2]
        assert curve not in sources[2]

    def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_ndoverlays(self):
        ndoverlay = hv.NdOverlay({i: hv.Area(range(10 + i)) for i in range(2)})
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve_redim = curve.redim(x="x2")
        combined = ndoverlay * curve_redim
        combined[()]
        sources = compute_overlayable_zorders(combined)

        assert ndoverlay[0] in sources[0]
        assert ndoverlay in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]

        assert ndoverlay[1] in sources[1]
        assert ndoverlay in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

        assert curve_redim in sources[2]
        assert curve in sources[2]
        assert ndoverlay not in sources[2]

    def test_dynamic_compute_overlayable_zorders_ndoverlays_as_input(self):
        ndoverlay1 = hv.NdOverlay({i: hv.Area(range(10 + i)) for i in range(2)}).apply(
            lambda el: el.get(0), dynamic=True
        )
        ndoverlay2 = hv.NdOverlay(
            {i: hv.Area((range(15, 25 + i), range(10 + i))) for i in range(2)}
        ).apply(lambda el: el.get(0), dynamic=True)
        combined = ndoverlay1 * ndoverlay2
        combined[()]
        sources = compute_overlayable_zorders(combined)
        assert len(sources) == 2

    def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_dynamic_ndoverlay_with_streams(
        self,
    ):
        ndoverlay = hv.DynamicMap(
            lambda x: hv.NdOverlay({i: hv.Area(range(10 + i)) for i in range(2)}),
            kdims=[],
            streams=[PointerX()],
        )
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve_redim = curve.redim(x="x2")
        combined = ndoverlay * curve_redim
        combined[()]
        sources = compute_overlayable_zorders(combined)

        assert ndoverlay in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]

        assert ndoverlay in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

        assert curve_redim in sources[2]
        assert curve in sources[2]
        assert ndoverlay not in sources[2]

    def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_dynamic_ndoverlay_with_streams_cloned(
        self,
    ):
        ndoverlay = hv.DynamicMap(
            lambda x: hv.NdOverlay({i: hv.Area(range(10 + i)) for i in range(2)}),
            kdims=[],
            streams=[PointerX()],
        )
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve_redim = curve.redim(x="x2")
        combined = ndoverlay * curve_redim
        combined[()]
        sources = compute_overlayable_zorders(combined.clone())

        assert ndoverlay in sources[0]
        assert curve_redim not in sources[0]
        assert curve not in sources[0]

        assert ndoverlay in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

        assert curve_redim in sources[2]
        assert curve in sources[2]
        assert ndoverlay not in sources[2]

    def test_dynamic_compute_overlayable_zorders_mixed_dynamic_and_non_dynamic_ndoverlays_reverse(
        self,
    ):
        ndoverlay = hv.NdOverlay({i: hv.Area(range(10 + i)) for i in range(2)})
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve_redim = curve.redim(x="x2")
        combined = curve_redim * ndoverlay
        combined[()]
        sources = compute_overlayable_zorders(combined)

        assert curve_redim in sources[0]
        assert curve in sources[0]
        assert ndoverlay not in sources[0]

        assert ndoverlay[0] in sources[1]
        assert ndoverlay in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

        assert ndoverlay[1] in sources[2]
        assert ndoverlay in sources[2]
        assert curve_redim not in sources[2]
        assert curve not in sources[2]

    def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_reduced(self):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve2 = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        area_redim = area.redim(x="x2")
        curve_redim = curve.redim(x="x2")
        curve2_redim = curve2.redim(x="x3")
        combined = (area_redim * curve_redim).map(lambda x: x.get(0), hv.Overlay)
        combined1 = combined * curve2_redim
        combined1[()]
        sources = compute_overlayable_zorders(combined1)

        assert curve_redim in sources[0]
        assert curve in sources[0]
        assert area_redim in sources[0]
        assert area in sources[0]
        assert curve2_redim not in sources[0]
        assert curve2 not in sources[0]

        assert curve2_redim in sources[1]
        assert curve2 in sources[1]
        assert area_redim not in sources[1]
        assert area not in sources[1]
        assert curve_redim not in sources[1]
        assert curve not in sources[1]

    def test_dynamic_compute_overlayable_zorders_three_deep_dynamic_layers_reduced_layers_by_one(
        self,
    ):
        area = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        area2 = hv.DynamicMap(lambda: hv.Area(range(10)), kdims=[])
        curve = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        curve2 = hv.DynamicMap(lambda: hv.Curve(range(10)), kdims=[])
        area_redim = area.redim(x="x2")
        curve_redim = curve.redim(x="x2")
        curve2_redim = curve2.redim(x="x3")
        combined = (area_redim * curve_redim * area2).map(
            lambda x: x.clone(x.items()[:2]), hv.Overlay
        )
        combined1 = combined * curve2_redim
        combined1[()]
        sources = compute_overlayable_zorders(combined1)

        assert curve_redim not in sources[0]
        assert curve not in sources[0]
        assert curve2_redim not in sources[0]
        assert curve2 not in sources[0]
        assert area not in sources[0]
        assert area_redim not in sources[0]
        assert area2 not in sources[0]

        assert area_redim not in sources[1]
        assert area not in sources[1]
        assert curve2_redim not in sources[1]
        assert curve2 not in sources[1]
        assert area2 not in sources[0]

        assert curve2_redim in sources[2]
        assert curve2 in sources[2]
        assert area_redim not in sources[2]
        assert area not in sources[2]
        assert area2 not in sources[0]
        assert curve_redim not in sources[2]
        assert curve not in sources[2]


class TestInitializeDynamic:
    def test_dynamicmap_default_initializes(self):
        dims = [hv.Dimension("N", default=5, range=(0, 10))]
        dmap = hv.DynamicMap(lambda N: hv.Curve([1, N, 5]), kdims=dims)
        initialize_dynamic(dmap)
        assert dmap.keys() == [5]

    def test_dynamicmap_numeric_values_initializes(self):
        dims = [hv.Dimension("N", values=[10, 5, 0])]
        dmap = hv.DynamicMap(lambda N: hv.Curve([1, N, 5]), kdims=dims)
        initialize_dynamic(dmap)
        assert dmap.keys() == [0]


class TestSplitDynamicMapOverlay:
    """
    Tests the split_dmap_overlay utility
    """

    def setup_method(self):
        self.dmap_element = hv.DynamicMap(lambda: hv.Image([]))
        self.dmap_overlay = hv.DynamicMap(lambda: hv.Overlay([hv.Curve([]), hv.Points([])]))
        self.dmap_ndoverlay = hv.DynamicMap(
            lambda: hv.NdOverlay({0: hv.Curve([]), 1: hv.Curve([])})
        )
        self.element = hv.Scatter([])
        self.el1, self.el2 = hv.Path([]), hv.HLine(0)
        self.overlay = hv.Overlay([self.el1, self.el2])
        self.ndoverlay = hv.NdOverlay({0: hv.VectorField([]), 1: hv.VectorField([])})

    def test_dmap_ndoverlay(self):
        test = self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [self.dmap_ndoverlay, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay(self):
        test = self.dmap_overlay
        initialize_dynamic(test)
        layers = [self.dmap_overlay, self.dmap_overlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_element_mul_dmap_overlay(self):
        test = self.dmap_element * self.dmap_overlay
        initialize_dynamic(test)
        layers = [self.dmap_element, self.dmap_overlay, self.dmap_overlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_element_mul_dmap_ndoverlay(self):
        test = self.dmap_element * self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [self.dmap_element, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_element_mul_element(self):
        test = self.dmap_element * self.element
        initialize_dynamic(test)
        layers = [self.dmap_element, self.element]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_element_mul_overlay(self):
        test = self.dmap_element * self.overlay
        initialize_dynamic(test)
        layers = [self.dmap_element, self.el1, self.el2]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_element_mul_ndoverlay(self):
        test = self.dmap_element * self.ndoverlay
        initialize_dynamic(test)
        layers = [self.dmap_element, self.ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_mul_dmap_ndoverlay(self):
        test = self.dmap_overlay * self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [self.dmap_overlay, self.dmap_overlay, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_mul_element(self):
        test = self.dmap_overlay * self.element
        initialize_dynamic(test)
        layers = [self.dmap_overlay, self.dmap_overlay, self.element]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_mul_overlay(self):
        test = self.dmap_overlay * self.overlay
        initialize_dynamic(test)
        layers = [self.dmap_overlay, self.dmap_overlay, self.el1, self.el2]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_all_combinations(self):
        test = (
            self.dmap_overlay
            * self.element
            * self.dmap_ndoverlay
            * self.overlay
            * self.dmap_element
            * self.ndoverlay
        )
        initialize_dynamic(test)
        layers = [
            self.dmap_overlay,
            self.dmap_overlay,
            self.element,
            self.dmap_ndoverlay,
            self.el1,
            self.el2,
            self.dmap_element,
            self.ndoverlay,
        ]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_operation_mul_dmap_ndoverlay(self):
        mapped = operation(self.dmap_overlay)
        test = mapped * self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [mapped, mapped, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_linked_operation_mul_dmap_ndoverlay(self):
        mapped = operation(self.dmap_overlay, link_inputs=True)
        test = mapped * self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [mapped, mapped, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers

    def test_dmap_overlay_linked_operation_mul_dmap_element_ndoverlay(self):
        mapped = self.dmap_overlay.map(lambda x: x.get(0), hv.Overlay)
        test = mapped * self.element * self.dmap_ndoverlay
        initialize_dynamic(test)
        layers = [mapped, self.element, self.dmap_ndoverlay]
        assert split_dmap_overlay(test)[0] == layers


class TestPlotColorUtils:
    def test_process_cmap_list_cycle(self):
        colors = process_cmap(["#ffffff", "#959595", "#000000"], 4)
        assert colors == ["#ffffff", "#959595", "#000000", "#ffffff"]

    def test_process_cmap_cycle(self):
        colors = process_cmap(hv.Cycle(values=["#ffffff", "#959595", "#000000"]), 4)
        assert colors == ["#ffffff", "#959595", "#000000", "#ffffff"]

    def test_process_cmap_invalid_str(self):
        with pytest.raises(ValueError):  # noqa: PT011
            process_cmap("NonexistentColorMap", 3)

    def test_process_cmap_invalid_type(self):
        with pytest.raises(TypeError):
            process_cmap({"A", "B", "C"}, 3)


@pytest.mark.usefixtures("mpl_backend")
class TestMPLColormapUtils:
    def test_mpl_colormap_fire(self):
        colors = process_cmap("fire", 3, provider="matplotlib")
        assert colors == ["#000000", "#ed1400", "#ffffff"]

    def test_mpl_colormap_fire_r(self):
        colors = process_cmap("fire_r", 3, provider="matplotlib")
        assert colors == ["#ffffff", "#eb1300", "#000000"]

    def test_mpl_colormap_name_palette(self):
        colors = process_cmap("Greys", 3, provider="matplotlib")
        assert colors == ["#ffffff", "#959595", "#000000"]

    def test_mpl_colormap_instance(self):
        try:
            from matplotlib import colormaps

            cmap = colormaps.get("Greys")
        except ImportError:
            # This will stop working and can be removed
            # when we only support Matplotlib >= 3.5
            from matplotlib.cm import get_cmap

            cmap = get_cmap("Greys")

        colors = process_cmap(cmap, 3, provider="matplotlib")
        assert colors == ["#ffffff", "#959595", "#000000"]

    def test_mpl_colormap_categorical(self):
        colors = mplcmap_to_palette("Category20", 3)
        assert colors == ["#1f77b4", "#c5b0d5", "#9edae5"]

    def test_mpl_colormap_categorical_reverse(self):
        colors = mplcmap_to_palette("Category20_r", 3)
        assert colors == ["#1f77b4", "#8c564b", "#9edae5"][::-1]

    def test_mpl_colormap_sequential(self):
        colors = mplcmap_to_palette("YlGn", 3)
        assert colors == ["#ffffe5", "#77c578", "#004529"]

    def test_mpl_colormap_sequential_reverse(self):
        colors = mplcmap_to_palette("YlGn_r", 3)
        assert colors == ["#ffffe5", "#78c679", "#004529"][::-1]

    def test_mpl_colormap_diverging(self):
        colors = mplcmap_to_palette("RdBu", 3)
        assert colors == ["#67001f", "#f6f6f6", "#053061"]

    def test_mpl_colormap_diverging_reverse(self):
        colors = mplcmap_to_palette("RdBu_r", 3)
        assert colors == ["#67001f", "#f7f6f6", "#053061"][::-1]

    def test_mpl_colormap_perceptually_uniform(self):
        colors = mplcmap_to_palette("viridis", 4)
        assert colors == ["#440154", "#30678d", "#35b778", "#fde724"]

    def test_mpl_colormap_perceptually_uniform_reverse(self):
        colors = mplcmap_to_palette("viridis_r", 4)
        assert colors == ["#440154", "#30678d", "#35b778", "#fde724"][::-1]


@pytest.mark.usefixtures("bokeh_backend")
class TestBokehPaletteUtils:
    def test_bokeh_palette_categorical_palettes_not_interpolated(self):
        # Ensure categorical palettes are not expanded
        categorical = (
            "accent",
            "category20",
            "dark2",
            "colorblind",
            "pastel1",
            "pastel2",
            "set1",
            "set2",
            "set3",
            "paired",
        )
        for cat in categorical:
            assert len(set(bokeh_palette_to_palette(cat))) <= 20

    def test_bokeh_colormap_fire(self):
        colors = process_cmap("fire", 3, provider="bokeh")
        assert colors == ["#000000", "#eb1300", "#ffffff"]

    def test_bokeh_colormap_fire_r(self):
        colors = process_cmap("fire_r", 3, provider="bokeh")
        assert colors == ["#ffffff", "#ed1400", "#000000"]

    def test_bokeh_palette_categorical(self):
        colors = bokeh_palette_to_palette("Category20", 3)
        assert colors == ["#1f77b4", "#c5b0d5", "#9edae5"]

    def test_bokeh_palette_categorical_reverse(self):
        colors = bokeh_palette_to_palette("Category20_r", 3)
        assert colors == ["#1f77b4", "#8c564b", "#9edae5"][::-1]

    def test_bokeh_palette_sequential(self):
        colors = bokeh_palette_to_palette("YlGn", 3)
        assert colors == ["#ffffe5", "#78c679", "#004529"]

    def test_bokeh_palette_sequential_reverse(self):
        colors = bokeh_palette_to_palette("YlGn_r", 3)
        assert colors == ["#ffffe5", "#78c679", "#004529"][::-1]

    def test_bokeh_palette_diverging(self):
        colors = bokeh_palette_to_palette("RdBu", 3)
        assert colors == ["#67001f", "#f7f7f7", "#053061"]

    def test_bokeh_palette_diverging_reverse(self):
        colors = bokeh_palette_to_palette("RdBu_r", 3)
        assert colors == ["#67001f", "#f7f7f7", "#053061"][::-1]

    def test_bokeh_palette_uniform_interpolated(self):
        colors = bokeh_palette_to_palette("Viridis", 4)
        assert colors == ["#440154", "#30678D", "#35B778", "#FDE724"]

    def test_bokeh_palette_perceptually_uniform(self):
        colors = bokeh_palette_to_palette("viridis", 4)
        assert colors == ["#440154", "#30678D", "#35B778", "#FDE724"]

    def test_bokeh_palette_perceptually_uniform_reverse(self):
        colors = bokeh_palette_to_palette("viridis_r", 4)
        assert colors == ["#440154", "#30678D", "#35B778", "#FDE724"][::-1]

    def test_color_intervals(self):
        levels = [0, 38, 73, 95, 110, 130, 156]
        colors = ["#5ebaff", "#00faf4", "#ffffcc", "#ffe775", "#ffc140", "#ff8f20"]
        cmap, _lims = color_intervals(colors, levels, N=10)
        assert cmap == [
            "#5ebaff",
            "#5ebaff",
            "#00faf4",
            "#00faf4",
            "#ffffcc",
            "#ffe775",
            "#ffc140",
            "#ff8f20",
            "#ff8f20",
        ]

    def test_color_intervals_clipped(self):
        levels = [0, 38, 73, 95, 110, 130, 156, 999]
        colors = ["#5ebaff", "#00faf4", "#ffffcc", "#ffe775", "#ffc140", "#ff8f20", "#ff6060"]
        cmap, lims = color_intervals(colors, levels, clip=(10, 90), N=100)
        assert cmap == [
            "#5ebaff",
            "#5ebaff",
            "#5ebaff",
            "#00faf4",
            "#00faf4",
            "#00faf4",
            "#00faf4",
            "#ffffcc",
            "#ffffcc",
        ]
        assert lims == (10, 90)


class TestPlotUtils:
    def test_get_min_distance_float32_type(self):
        xs, ys = (np.arange(0, 2.0, 0.2, dtype="float32"), np.arange(0, 2.0, 0.2, dtype="float32"))
        X, Y = np.meshgrid(xs, ys)
        dist = get_min_distance(hv.Points((X.flatten(), Y.flatten())))
        assert np.isclose(dist, 0.2)

    def test_get_min_distance_int32_type(self):
        xs, ys = (np.arange(0, 10, dtype="int32"), np.arange(0, 10, dtype="int32"))
        X, Y = np.meshgrid(xs, ys)
        dist = get_min_distance(hv.Points((X.flatten(), Y.flatten())))
        assert dist == 1.0

    def test_get_min_distance_float32_type_no_scipy(self):
        xs, ys = (np.arange(0, 2.0, 0.2, dtype="float32"), np.arange(0, 2.0, 0.2, dtype="float32"))
        X, Y = np.meshgrid(xs, ys)
        dist = _get_min_distance_numpy(hv.Points((X.flatten(), Y.flatten())))
        assert np.isclose(dist, np.float32(0.2))

    def test_get_min_distance_int32_type_no_scipy(self):
        xs, ys = (np.arange(0, 10, dtype="int32"), np.arange(0, 10, dtype="int32"))
        X, Y = np.meshgrid(xs, ys)
        dist = _get_min_distance_numpy(hv.Points((X.flatten(), Y.flatten())))
        assert dist == 1.0


class TestRangeUtilities:
    def test_get_axis_padding_scalar(self):
        padding = get_axis_padding(0.1)
        assert padding == (0.1, 0.1, 0.1)

    def test_get_axis_padding_tuple(self):
        padding = get_axis_padding((0.1, 0.2))
        assert padding == (0.1, 0.2, 0)

    def test_get_axis_padding_tuple_3d(self):
        padding = get_axis_padding((0.1, 0.2, 0.3))
        assert padding == (0.1, 0.2, 0.3)

    def test_get_range_from_element(self):
        dim = hv.Dimension("y", soft_range=(0, 3), range=(0, 2))
        element = hv.Scatter([1, 2, 3], vdims=dim)
        drange, srange, hrange = get_range(element, {}, dim)
        assert drange == (1, 3)
        assert srange == (0, 3)
        assert hrange == (0, 2)

    def test_get_range_from_ranges(self):
        dim = hv.Dimension("y", soft_range=(0, 3), range=(0, 2))
        element = hv.Scatter([1, 2, 3], vdims=dim)
        ranges = {"y": {"soft": (-1, 4), "hard": (-1, 3), "data": (-0.5, 2.5)}}
        drange, srange, hrange = get_range(element, ranges, dim)
        assert drange == (-0.5, 2.5)
        assert srange == (-1, 4)
        assert hrange == (-1, 3)
