import datetime as dt
import random

import numpy as np
import pandas as pd
import param
import pytest

import holoviews as hv
from holoviews.core.data.grid import GridInterface
from holoviews.core.data.ibis import IBIS_VERSION
from holoviews.core.options import Compositor, SkipRendering
from holoviews.operation.element import (
    chain,
    contours,
    decimate,
    dendrogram,
    gradient,
    histogram,
    interpolate_curve,
    operation,
    threshold,
    transform,
)
from holoviews.testing import assert_element_equal

from ..utils import optional_dependencies

mpl, mpl_skip = optional_dependencies("matplotlib")
da, dask_skip = optional_dependencies("dask.array")
ibis, ibis_skip = optional_dependencies("ibis")


class OperationTests:
    """
    Tests allowable data formats when constructing
    the basic Element types.
    """

    def test_operation_element(self):
        img = hv.Image(np.random.rand(10, 10))
        op_img = operation(img, op=lambda x, k: x.clone(x.data * 2))
        assert_element_equal(op_img, img.clone(img.data * 2, group="Operation"))

    def test_operation_ndlayout(self):
        ndlayout = hv.NdLayout({i: hv.Image(np.random.rand(10, 10)) for i in range(10)})
        op_ndlayout = operation(ndlayout, op=lambda x, k: x.clone(x.data * 2))
        doubled = ndlayout.clone(
            {k: v.clone(v.data * 2, group="Operation") for k, v in ndlayout.items()}
        )
        assert_element_equal(op_ndlayout, doubled)

    def test_operation_grid(self):
        grid = hv.GridSpace({i: hv.Image(np.random.rand(10, 10)) for i in range(10)}, kdims=["X"])
        op_grid = operation(grid, op=lambda x, k: x.clone(x.data * 2))
        doubled = grid.clone({k: v.clone(v.data * 2, group="Operation") for k, v in grid.items()})
        assert_element_equal(op_grid, doubled)

    def test_operation_holomap(self):
        hmap = hv.HoloMap({1: hv.Image(np.random.rand(10, 10))})
        op_hmap = operation(hmap, op=lambda x, k: x.clone(x.data * 2))
        assert_element_equal(op_hmap.last, hmap.last.clone(hmap.last.data * 2, group="Operation"))

    def test_image_transform(self):
        img = hv.Image(np.random.rand(10, 10))
        op_img = transform(img, operator=lambda x: x * 2)
        assert_element_equal(op_img, img.clone(img.data * 2, group="Transform"))

    def test_operation_chain(self):
        img = hv.Image(np.random.rand(10, 10))
        op_img = chain(
            img,
            operations=[
                transform.instance(operator=lambda x: x * 2),
                transform.instance(operator=lambda x: x * 3),
            ],
        )
        assert_element_equal(op_img, img.clone(img.data * 6, group="Transform"))

    def test_operation_chain_find(self):
        class CustomOp1(hv.Operation):
            link_inputs = param.Boolean(False)

        class CustomOp2(hv.Operation):
            link_inputs = param.Boolean(True)

        op1 = CustomOp1.instance()
        op2 = CustomOp2.instance()
        ch_op = chain.instance(operations=[op1, op2])
        assert ch_op.find(CustomOp1, skip_nonlinked=False) is op1
        assert ch_op.find(CustomOp1, skip_nonlinked=True) is None
        assert ch_op.find(CustomOp2, skip_nonlinked=False) is op2
        assert ch_op.find(CustomOp2, skip_nonlinked=True) is op2

    def test_operation_chain_find_apply(self):
        img = hv.Image(np.random.rand(10, 10))
        tr_op = transform.instance(operator=lambda x: x * 2)
        img_apply = img.apply(tr_op, dynamic=False)
        assert img_apply.pipeline.find(transform, skip_nonlinked=False) is tr_op

    def test_operation_chain_find_apply_chain(self):
        class CustomOp1(hv.Operation):
            pass

        class CustomOp2(hv.Operation):
            pass

        img = hv.Image(np.random.rand(10, 10))
        op1 = CustomOp1.instance()
        op2 = CustomOp2.instance()
        ch_op = chain.instance(operations=[op1, op2])
        img_apply = img.apply(ch_op, dynamic=False)
        assert img_apply.pipeline.find(CustomOp1, skip_nonlinked=False) is op1

    def test_image_threshold(self):
        img = hv.Image(np.array([[0, 1, 0], [3, 4, 5.0]]))
        op_img = threshold(img)
        assert_element_equal(
            op_img, img.clone(np.array([[0, 1, 0], [1, 1, 1]]), group="Threshold")
        )

    def test_image_gradient(self):
        img = hv.Image(np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]]))
        op_img = gradient(img)
        assert_element_equal(
            op_img,
            img.clone(np.array([[3.162278, 3.162278], [3.162278, 3.162278]]), group="Gradient"),
        )

    def test_image_contours(self):
        img = hv.Image(np.array([[0, 1, 0], [0, 1, 0]]))
        op_contours = contours(img, levels=[0.5])
        # Note multiple lines which are nan-separated.
        contour = hv.Contours(
            [
                [
                    (-0.166667, 0.25, 0.5),
                    (-0.1666667, -0.25, 0.5),
                    (np.nan, np.nan, 0.5),
                    (0.1666667, -0.25, 0.5),
                    (0.1666667, 0.25, 0.5),
                ]
            ],
            vdims=img.vdims,
        )
        assert_element_equal(op_contours, contour)

    def test_image_contours_empty(self):
        img = hv.Image(np.array([[0, 1, 0], [0, 1, 0]]))
        # Contour level outside of data limits
        op_contours = contours(img, levels=[23.0])
        contour = hv.Contours([], vdims=img.vdims)
        assert_element_equal(op_contours, contour)

    def test_image_contours_auto_levels(self):
        z = np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]])
        img = hv.Image(z)
        for nlevels in range(3, 20):
            op_contours = contours(img, levels=nlevels)
            levels = [item["z"] for item in op_contours.data]
            assert len(levels) <= nlevels + 2
            assert np.min(levels) <= z.min()
            assert np.max(levels) < z.max()

    def test_image_contours_no_range(self):
        img = hv.Image(np.zeros((2, 2)))
        op_contours = contours(img, levels=2)
        contour = hv.Contours([], vdims=img.vdims)
        assert_element_equal(op_contours, contour)

    @mpl_skip
    def test_image_contours_x_datetime(self):
        x = np.array(["2023-09-01", "2023-09-03", "2023-09-05"], dtype="datetime64")
        y = [14, 15]
        z = np.array([[0, 1, 0], [0, 1, 0]])
        img = hv.Image((x, y, z))
        op_contours = contours(img, levels=[0.5])
        # Note multiple lines which are nan-separated.
        tz = dt.timezone.utc
        expected_x = np.array(
            [
                dt.datetime(2023, 9, 2, tzinfo=tz),
                dt.datetime(2023, 9, 2, tzinfo=tz),
                np.nan,
                dt.datetime(2023, 9, 4, tzinfo=tz),
                dt.datetime(2023, 9, 4, tzinfo=tz),
            ],
            dtype=object,
        )

        # Separately compare nans and datetimes
        x = op_contours.dimension_values("x")
        mask = np.array([True, True, False, True, True])  # Mask ignoring nans
        np.testing.assert_array_equal(x[mask], expected_x[mask])
        np.testing.assert_array_equal(x[~mask].astype(float), expected_x[~mask].astype(float))

        np.testing.assert_array_almost_equal(
            op_contours.dimension_values("y").astype(float), [15, 14, np.nan, 14, 15]
        )
        np.testing.assert_array_almost_equal(op_contours.dimension_values("z"), [0.5] * 5)

    @mpl_skip
    def test_image_contours_y_datetime(self):
        x = [14, 15, 16]
        y = np.array(["2023-09-01", "2023-09-03"], dtype="datetime64")
        z = np.array([[0, 1, 0], [0, 1, 0]])
        img = hv.Image((x, y, z))
        op_contours = contours(img, levels=[0.5])
        # Note multiple lines which are nan-separated.
        np.testing.assert_array_almost_equal(
            op_contours.dimension_values("x").astype(float), [14.5, 14.5, np.nan, 15.5, 15.5]
        )

        tz = dt.timezone.utc
        expected_y = np.array(
            [
                dt.datetime(2023, 9, 3, tzinfo=tz),
                dt.datetime(2023, 9, 1, tzinfo=tz),
                np.nan,
                dt.datetime(2023, 9, 1, tzinfo=tz),
                dt.datetime(2023, 9, 3, tzinfo=tz),
            ],
            dtype=object,
        )

        # Separately compare nans and datetimes
        y = op_contours.dimension_values("y")
        mask = np.array([True, True, False, True, True])  # Mask ignoring nans
        np.testing.assert_array_equal(y[mask], expected_y[mask])
        np.testing.assert_array_equal(y[~mask].astype(float), expected_y[~mask].astype(float))

        np.testing.assert_array_almost_equal(op_contours.dimension_values("z"), [0.5] * 5)

    @mpl_skip
    def test_image_contours_xy_datetime(self):
        x = np.array(["2023-09-01", "2023-09-03", "2023-09-05"], dtype="datetime64")
        y = np.array(["2023-10-07", "2023-10-08"], dtype="datetime64")
        z = np.array([[0, 1, 0], [0, 1, 0]])
        img = hv.Image((x, y, z))
        op_contours = contours(img, levels=[0.5])
        # Note multiple lines which are nan-separated.

        tz = dt.timezone.utc
        expected_x = np.array(
            [
                dt.datetime(2023, 9, 2, tzinfo=tz),
                dt.datetime(2023, 9, 2, tzinfo=tz),
                np.nan,
                dt.datetime(2023, 9, 4, tzinfo=tz),
                dt.datetime(2023, 9, 4, tzinfo=tz),
            ],
            dtype=object,
        )
        expected_y = np.array(
            [
                dt.datetime(2023, 10, 8, tzinfo=tz),
                dt.datetime(2023, 10, 7, tzinfo=tz),
                np.nan,
                dt.datetime(2023, 10, 7, tzinfo=tz),
                dt.datetime(2023, 10, 8, tzinfo=tz),
            ],
            dtype=object,
        )

        # Separately compare nans and datetimes
        x = op_contours.dimension_values("x")
        mask = np.array([True, True, False, True, True])  # Mask ignoring nans
        np.testing.assert_array_equal(x[mask], expected_x[mask])
        np.testing.assert_array_equal(x[~mask].astype(float), expected_x[~mask].astype(float))

        y = op_contours.dimension_values("y")
        np.testing.assert_array_equal(y[mask], expected_y[mask])
        np.testing.assert_array_equal(y[~mask].astype(float), expected_y[~mask].astype(float))

        np.testing.assert_array_almost_equal(op_contours.dimension_values("z"), [0.5] * 5)

    @mpl_skip
    def test_image_contours_z_datetime(self):
        z = np.array(
            [["2023-09-10", "2023-09-10"], ["2023-09-10", "2023-09-12"]], dtype="datetime64"
        )
        img = hv.Image(z)
        op_contours = contours(img, levels=[np.datetime64("2023-09-11")])
        np.testing.assert_array_almost_equal(op_contours.dimension_values("x"), [0.25, 0.0])
        np.testing.assert_array_almost_equal(op_contours.dimension_values("y"), [0.0, -0.25])
        expected_z = np.array(
            [
                dt.datetime(2023, 9, 11, 0, 0, tzinfo=dt.timezone.utc),
                dt.datetime(2023, 9, 11, 0, 0, tzinfo=dt.timezone.utc),
            ],
            dtype=object,
        )
        np.testing.assert_array_equal(op_contours.dimension_values("z"), expected_z)

    def test_qmesh_contours(self):
        qmesh = hv.QuadMesh(([0, 1, 2], [1, 2, 3], np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]])))
        op_contours = contours(qmesh, levels=[0.5])
        contour = hv.Contours(
            [
                [
                    (0, 1.166667, 0.5),
                    (0.5, 1.0, 0.5),
                    (np.nan, np.nan, 0.5),
                    (1.5, 1.0, 0.5),
                    (2, 1.1, 0.5),
                ]
            ],
            vdims=qmesh.vdims,
        )
        assert_element_equal(op_contours, contour)

    def test_qmesh_curvilinear_contours(self):
        x = y = np.arange(3)
        xs, ys = np.meshgrid(x, y)
        zs = np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]])
        qmesh = hv.QuadMesh((xs, ys + 0.1, zs))
        op_contours = contours(qmesh, levels=[0.5])
        contour = hv.Contours(
            [
                [
                    (0, 0.266667, 0.5),
                    (0.5, 0.1, 0.5),
                    (np.nan, np.nan, 0.5),
                    (1.5, 0.1, 0.5),
                    (2, 0.2, 0.5),
                ]
            ],
            vdims=qmesh.vdims,
        )
        assert_element_equal(op_contours, contour)

    def test_qmesh_curvilinear_edges_contours(self):
        x = y = np.arange(3)
        xs, ys = np.meshgrid(x, y)
        xs = GridInterface._infer_interval_breaks(xs)
        xs = GridInterface._infer_interval_breaks(xs, 1)
        ys = GridInterface._infer_interval_breaks(ys)
        ys = GridInterface._infer_interval_breaks(ys, 1)
        zs = np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]])
        qmesh = hv.QuadMesh((xs, ys + 0.1, zs))
        op_contours = contours(qmesh, levels=[0.5])
        contour = hv.Contours(
            [
                [
                    (0, 0.266667, 0.5),
                    (0.5, 0.1, 0.5),
                    (np.nan, np.nan, 0.5),
                    (1.5, 0.1, 0.5),
                    (2, 0.2, 0.5),
                ]
            ],
            vdims=qmesh.vdims,
        )
        assert_element_equal(op_contours, contour)

    def test_image_contours_filled(self):
        img = hv.Image(np.array([[0, 2, 0], [0, 2, 0]]))
        # Two polygons (nan-separated) without holes
        op_contours = contours(img, filled=True, levels=[0.5, 1.5])
        data = [
            [
                (-0.25, -0.25, 1),
                (-0.08333333, -0.25, 1),
                (-0.08333333, 0.25, 1),
                (-0.25, 0.25, 1),
                (-0.25, -0.25, 1),
                (np.nan, np.nan, 1),
                (0.08333333, -0.25, 1),
                (0.25, -0.25, 1),
                (0.25, 0.25, 1),
                (0.08333333, 0.25, 1),
                (0.08333333, -0.25, 1),
            ]
        ]
        polys = hv.Polygons(data, vdims=img.vdims[0].clone(range=(0.5, 1.5)))
        assert_element_equal(op_contours, polys)

    def test_image_contours_filled_with_hole(self):
        img = hv.Image(np.array([[0, 0, 0], [0, 1, 0.0], [0, 0, 0]]))
        # Single polygon with hole
        op_contours = contours(img, filled=True, levels=[0.25, 0.75])
        data = [
            [
                (-0.25, 0.0, 0.5),
                (0.0, -0.25, 0.5),
                (0.25, 0.0, 0.5),
                (0.0, 0.25, 0.5),
                (-0.25, 0.0, 0.5),
            ]
        ]
        polys = hv.Polygons(data, vdims=img.vdims[0].clone(range=(0.25, 0.75)))
        assert_element_equal(op_contours, polys)
        expected_holes = [
            [
                [
                    np.array(
                        [
                            [0.0, -0.08333333],
                            [-0.08333333, 0.0],
                            [0.0, 0.08333333],
                            [0.08333333, 0.0],
                            [0.0, -0.08333333],
                        ]
                    )
                ]
            ]
        ]
        np.testing.assert_array_almost_equal(op_contours.holes(), expected_holes)

    def test_image_contours_filled_multi_holes(self):
        img = hv.Image(np.array([[0, 0, 0, 0, 0], [0, 1, 0, 1, 0], [0, 0, 0, 0, 0]]))
        # Single polygon with two holes
        op_contours = contours(img, filled=True, levels=[-0.5, 0.5])
        data = [
            [
                (-0.4, -0.3333333, 0),
                (-0.2, -0.3333333, 0),
                (0, -0.3333333, 0),
                (0.2, -0.3333333, 0),
                (0.4, -0.3333333, 0),
                (0.4, 0, 0),
                (0.4, 0.3333333, 0),
                (0.2, 0.3333333, 0),
                (0, 0.3333333, 0),
                (-0.2, 0.3333333, 0),
                (-0.4, 0.3333333, 0),
                (-0.4, 0, 0),
                (-0.4, -0.3333333, 0),
            ]
        ]
        polys = hv.Polygons(data, vdims=img.vdims[0].clone(range=(-0.5, 0.5)))
        assert_element_equal(op_contours, polys)
        hole1 = np.array(
            [
                [-0.2, -0.16666667],
                [-0.3, 0],
                [-0.2, 0.16666667],
                [-0.1, 0],
                [-0.2, -0.16666667],
            ]
        )
        hole2 = np.array(
            [
                [0.2, -0.16666667],
                [0.1, 0],
                [0.2, 0.16666667],
                [0.3, 0],
                [0.2, -0.16666667],
            ]
        )
        expected_holes = [[[hole1, hole2]]]
        np.testing.assert_array_almost_equal(op_contours.holes(), expected_holes)

    def test_image_contours_filled_empty(self):
        img = hv.Image(np.array([[0, 1, 0], [3, 4, 5.0], [6, 7, 8]]))
        # Contour level outside of data limits
        op_contours = contours(img, filled=True, levels=[20.0, 23.0])
        polys = hv.Polygons([], vdims=img.vdims[0].clone(range=(20.0, 23.0)))
        assert_element_equal(op_contours, polys)

    def test_image_contours_filled_auto_levels(self):
        z = np.array([[0, 1, 0], [3, 4, 5], [6, 7, 8]])
        img = hv.Image(z)
        for nlevels in range(3, 20):
            op_contours = contours(img, filled=True, levels=nlevels)
            levels = [item["z"] for item in op_contours.data]
            assert len(levels) <= nlevels + 1
            delta = 0.5 * (levels[1] - levels[0])
            assert np.min(levels) <= z.min() + delta
            assert np.max(levels) >= z.max() - delta

    def test_image_contours_filled_x_datetime(self):
        x = np.array(["2023-09-01", "2023-09-05", "2023-09-09"], dtype="datetime64")
        y = np.array([6, 7])
        z = np.array([[0, 2, 0], [0, 2, 0]])
        img = hv.Image((x, y, z))
        msg = r"Datetime spatial coordinates are not supported for filled contour calculations."
        with pytest.raises(RuntimeError, match=msg):
            _ = contours(img, filled=True, levels=[0.5, 1.5])

    def test_points_histogram(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(points, num_bins=3, normed=True)

        hist = hv.Histogram(
            ([0, 3, 6, 9], [0.1, 0.1, 0.133333]), vdims=("x_frequency", "Frequency")
        )
        assert_element_equal(op_hist, hist)

    def test_dataset_histogram_empty_explicit_bins(self):
        ds = hv.Dataset([np.nan, np.nan], ["x"])
        op_hist = histogram(ds, bins=[0, 1, 2])

        hist = hv.Histogram(([0, 1, 2], [0, 0]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_dataset_histogram_groupby_range_shared(self):
        x = np.arange(10)
        y = np.arange(10) + 10
        xy = np.concatenate([x, y])
        label = ["x"] * 10 + ["y"] * 10

        ds = hv.Dataset(pd.DataFrame([xy, label], index=["xy", "label"]).T, vdims=["xy", "label"])
        hist = histogram(ds, groupby="label", groupby_range="shared")
        exp = np.linspace(0, 19, 21)
        for k, v in hist.items():
            np.testing.assert_equal(exp, v.data["xy"])
            sel = np.asarray(label) == k
            assert sel.sum() == 10
            assert (v.data["xy_count"][sel] == 1).all()
            assert (v.data["xy_count"][~sel] == 0).all()

    def test_dataset_histogram_groupby_range_separated(self):
        x = np.arange(10)
        y = np.arange(10) + 10
        xy = np.concatenate([x, y])
        label = ["x"] * 10 + ["y"] * 10

        ds = hv.Dataset(pd.DataFrame([xy, label], index=["xy", "label"]).T, vdims=["xy", "label"])
        hist = histogram(ds, groupby="label", groupby_range="separated")

        for idx, v in enumerate(hist):
            exp = np.linspace(idx * 10, 10 * idx + 9, 21)
            np.testing.assert_equal(exp, v.data["xy"])
            assert v.data["xy_count"].sum() == 10

    def test_dataset_histogram_groupby_datetime(self):
        x = pd.date_range("2020-01-01", periods=100)
        y = pd.date_range("2020-01-01", periods=100)
        xy = np.concatenate([x, y])
        label = ["x"] * 100 + ["y"] * 100
        ds = hv.Dataset(pd.DataFrame([xy, label], index=["xy", "label"]).T, vdims=["xy", "label"])
        hist = histogram(ds, groupby="label")

        exp = pd.date_range("2020-01-01", "2020-04-09", periods=21)
        for h in hist:
            np.testing.assert_equal(exp, h.data["xy"])
            assert (h.data["xy_count"] == 5).all()

    @dask_skip
    def test_dataset_histogram_dask(self):
        import dask.array as da

        ds = hv.Dataset(
            (da.from_array(np.array(range(10), dtype="f"), chunks=(3)),), ["x"], datatype=["dask"]
        )
        op_hist = histogram(ds, num_bins=3, normed=True)

        hist = hv.Histogram(
            ([0, 3, 6, 9], [0.1, 0.1, 0.133333]), vdims=("x_frequency", "Frequency")
        )
        assert isinstance(op_hist.data["x_frequency"], da.Array)
        assert_element_equal(op_hist, hist)

    @dask_skip
    def test_dataset_cumulative_histogram_dask(self):
        import dask.array as da

        ds = hv.Dataset(
            (da.from_array(np.array(range(10), dtype="f"), chunks=(3)),), ["x"], datatype=["dask"]
        )
        op_hist = histogram(ds, num_bins=3, cumulative=True, normed=True)

        hist = hv.Histogram(([0, 3, 6, 9], [0.3, 0.6, 1]), vdims=("x_frequency", "Frequency"))
        assert isinstance(op_hist.data["x_frequency"], da.Array)
        assert_element_equal(op_hist, hist)

    @dask_skip
    def test_dataset_weighted_histogram_dask(self):
        import dask.array as da

        ds = hv.Dataset(
            (
                da.from_array(np.array(range(10), dtype="f"), chunks=3),
                da.from_array([i / 10.0 for i in range(10)], chunks=3),
            ),
            ["x", "y"],
            datatype=["dask"],
        )
        op_hist = histogram(ds, weight_dimension="y", num_bins=3, normed=True)

        hist = hv.Histogram(([0, 3, 6, 9], [0.022222, 0.088889, 0.222222]), vdims="y")
        assert isinstance(op_hist.data["y"], da.Array)
        assert_element_equal(op_hist, hist)

    @ibis_skip
    @pytest.mark.usefixtures("ibis_sqlite_backend")
    def test_dataset_histogram_ibis(self):
        df = pd.DataFrame(dict(x=np.arange(10)))
        t = ibis.memtable(df, **({} if IBIS_VERSION >= (11, 0, 0) else {"name": "t"}))
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, dimension="x", num_bins=3, normed=True)

        hist = hv.Histogram(
            ([0, 3, 6, 9], [0.1, 0.1, 0.133333]), vdims=("x_frequency", "Frequency")
        )
        assert_element_equal(op_hist, hist)

    @ibis_skip
    @pytest.mark.usefixtures("ibis_sqlite_backend")
    def test_dataset_cumulative_histogram_ibis(self):
        df = pd.DataFrame(dict(x=np.arange(10)))
        t = ibis.memtable(df, **({} if IBIS_VERSION >= (11, 0, 0) else {"name": "t"}))
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, num_bins=3, cumulative=True, normed=True)

        hist = hv.Histogram(([0, 3, 6, 9], [0.3, 0.6, 1]), vdims=("x_frequency", "Frequency"))
        assert_element_equal(op_hist, hist)

    @ibis_skip
    @pytest.mark.usefixtures("ibis_sqlite_backend")
    def test_dataset_histogram_explicit_bins_ibis(self):
        df = pd.DataFrame(dict(x=np.arange(10)))
        t = ibis.memtable(df, **({} if IBIS_VERSION >= (11, 0, 0) else {"name": "t"}))
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, bins=[0, 1, 3], normed=False)

        hist = hv.Histogram(([0, 1, 3], [1, 3]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    @pytest.mark.gpu
    def test_dataset_histogram_cudf(self):
        import cudf

        df = pd.DataFrame(dict(x=np.arange(10)))
        t = cudf.from_pandas(df)
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, dimension="x", num_bins=3, normed=True)

        hist = hv.Histogram(
            ([0, 3, 6, 9], [0.1, 0.1, 0.133333]), vdims=("x_frequency", "Frequency")
        )
        assert_element_equal(op_hist, hist)

    @pytest.mark.gpu
    def test_dataset_cumulative_histogram_cudf(self):
        import cudf

        df = pd.DataFrame(dict(x=np.arange(10)))
        t = cudf.from_pandas(df)
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, num_bins=3, cumulative=True, normed=True)

        hist = hv.Histogram(([0, 3, 6, 9], [0.3, 0.6, 1]), vdims=("x_frequency", "Frequency"))
        assert_element_equal(op_hist, hist)

    @pytest.mark.gpu
    def test_dataset_histogram_explicit_bins_cudf(self):
        import cudf

        df = pd.DataFrame(dict(x=np.arange(10)))
        t = cudf.from_pandas(df)
        ds = hv.Dataset(t, vdims="x")
        op_hist = histogram(ds, bins=[0, 1, 3], normed=False)

        hist = hv.Histogram(([0, 1, 3], [1, 3]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_points_histogram_bin_range(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(points, num_bins=3, bin_range=(0, 3), normed=True)

        hist = hv.Histogram(
            ([0.25, 0.25, 0.5], [0.0, 1.0, 2.0, 3.0]), vdims=("x_frequency", "Frequency")
        )
        assert_element_equal(op_hist, hist)

    def test_points_histogram_explicit_bins(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(points, bins=[0, 1, 3], normed=False)

        hist = hv.Histogram(([0, 1, 3], [1, 3]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_points_histogram_cumulative(self):
        arr = np.arange(4)
        points = hv.Points(arr)
        op_hist = histogram(points, cumulative=True, num_bins=3, normed=False)

        hist = hv.Histogram(([0, 1, 2, 3], [1, 2, 4]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_points_histogram_not_normed(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(points, num_bins=3, normed=False)

        hist = hv.Histogram(([0, 3, 6, 9], [3, 3, 4]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_histogram_operation_datetime(self):
        dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)])
        op_hist = histogram(hv.Dataset(dates, "Date"), num_bins=4, normed=True)
        hist_data = {
            "Date": np.array(
                [
                    "2017-01-01T00:00:00.000000",
                    "2017-01-01T18:00:00.000000",
                    "2017-01-02T12:00:00.000000",
                    "2017-01-03T06:00:00.000000",
                    "2017-01-04T00:00:00.000000",
                ],
                dtype="datetime64[us]",
            ),
            "Date_frequency": np.array(
                [3.85802469e-18, 3.85802469e-18, 3.85802469e-18, 3.85802469e-18]
            ),
        }
        hist = hv.Histogram(hist_data, kdims="Date", vdims=("Date_frequency", "Frequency"))
        assert_element_equal(op_hist, hist)

    def test_histogram_operation_datetime64(self):
        dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)]).astype("M")
        op_hist = histogram(hv.Dataset(dates, "Date"), num_bins=4, normed=True)
        hist_data = {
            "Date": np.array(
                [
                    "2017-01-01T00:00:00.000000",
                    "2017-01-01T18:00:00.000000",
                    "2017-01-02T12:00:00.000000",
                    "2017-01-03T06:00:00.000000",
                    "2017-01-04T00:00:00.000000",
                ],
                dtype="datetime64[us]",
            ),
            "Date_frequency": np.array(
                [3.85802469e-18, 3.85802469e-18, 3.85802469e-18, 3.85802469e-18]
            ),
        }
        hist = hv.Histogram(hist_data, kdims="Date", vdims=("Date_frequency", "Frequency"))
        assert_element_equal(op_hist, hist)

    def test_histogram_operation_pd_period(self):
        dates = pd.date_range("2017-01-01", "2017-01-04", freq="D").to_period("D")
        op_hist = histogram(hv.Dataset(dates, "Date"), num_bins=4, normed=True)
        hist_data = {
            "Date": np.array(
                [
                    "2017-01-01T00:00:00.000000",
                    "2017-01-01T18:00:00.000000",
                    "2017-01-02T12:00:00.000000",
                    "2017-01-03T06:00:00.000000",
                    "2017-01-04T00:00:00.000000",
                ],
                dtype="datetime64[us]",
            ),
            "Date_frequency": np.array(
                [3.85802469e-18, 3.85802469e-18, 3.85802469e-18, 3.85802469e-18]
            ),
        }
        hist = hv.Histogram(hist_data, kdims="Date", vdims=("Date_frequency", "Frequency"))
        assert_element_equal(op_hist, hist)

    def test_histogram_categorical(self):
        series = hv.Dataset(pd.Series(["A", "B", "C"]))
        kwargs = {"bin_range": ("A", "C"), "normed": False, "cumulative": False, "num_bins": 3}
        with pytest.raises(ValueError):  # noqa: PT011
            histogram(series, **kwargs)

    def test_points_histogram_weighted(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(points, num_bins=3, weight_dimension="y", normed=True)
        hist = hv.Histogram(([0.022222, 0.088889, 0.222222], [0, 3, 6, 9]), vdims=["y"])
        assert_element_equal(op_hist, hist)

    def test_points_histogram_mean_weighted(self):
        points = hv.Points([float(i) for i in range(10)])
        op_hist = histogram(
            points, num_bins=3, weight_dimension="y", mean_weighted=True, normed=True
        )
        hist = hv.Histogram(([1.0, 4.0, 7.5], [0, 3, 6, 9]), vdims=["y"])
        assert_element_equal(op_hist, hist)

    def test_histogram_narwhals_pandas(self):
        df = pd.DataFrame({"x": range(10)})
        ds = hv.Dataset(df, vdims="x", datatype=["narwhals"])
        op_hist = histogram(ds, num_bins=3, normed=False)

        hist = hv.Histogram(([0, 3, 6, 9], [3, 3, 4]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_histogram_narwhals_polars(self):
        pl = pytest.importorskip("polars")
        df = pl.DataFrame({"x": range(10)})
        ds = hv.Dataset(df, vdims="x")
        op_hist = histogram(ds, num_bins=3, normed=False)

        hist = hv.Histogram(([0, 3, 6, 9], [3, 3, 4]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_histogram_narwhals_polars_lazy(self):
        pl = pytest.importorskip("polars")
        df = pl.LazyFrame({"x": range(10)})
        ds = hv.Dataset(df, vdims="x")
        op_hist = histogram(ds, num_bins=3, normed=False)

        hist = hv.Histogram(([0, 3, 6, 9], [3, 3, 4]), vdims=("x_count", "Count"))
        assert_element_equal(op_hist, hist)

    def test_histogram_narwhals_polars_lazy_groupby(self):
        pl = pytest.importorskip("polars")
        df = pl.LazyFrame({"x": [1, 2], "y": [1, 2]})
        ds = hv.Dataset(df)
        op_hist = histogram(ds, num_bins=2, groupby="x")

        hist1 = hv.Histogram(([1, 1.5, 2], [1, 0]), kdims="y", vdims=[("y_count", "Count")])
        hist2 = hv.Histogram(([1, 1.5, 2], [0, 1]), kdims="y", vdims=[("y_count", "Count")])
        expected = hv.NdOverlay({(1,): hist1, (2,): hist2}, kdims=["x"])

        assert_element_equal(op_hist, expected)

    @pytest.mark.usefixtures("mpl_backend")
    def test_histogram_dask_array_mpl(self):
        # Regression test for https://github.com/holoviz/holoviews/issues/5111
        dd = pytest.importorskip("dask.dataframe")

        data = {
            "carrier": ["A", "A", "A", "A", "B", "B", "B", "B", "B"],
            "depdelay": [127.0, 3.0, -3.0, 19.0, 264.0, -6.0, 1.0, 2.0, 83.0],
        }
        flights = dd.from_pandas(pd.DataFrame(data), npartitions=2)

        by = "carrier"
        ds = hv.Dataset(flights, by)
        ds_grouped = ds.groupby(by)
        hists = histogram(ds_grouped, dimension="depdelay")

        # Should not error
        hv.renderer("matplotlib").get_plot(hists)

    def test_interpolate_curve_pre(self):
        interpolated = interpolate_curve(hv.Curve([0, 0.5, 1]), interpolation="steps-pre")
        curve = hv.Curve([(0, 0), (0, 0.5), (1, 0.5), (1, 1), (2, 1)])
        assert_element_equal(interpolated, curve)

    def test_interpolate_curve_pre_with_values(self):
        interpolated = interpolate_curve(
            hv.Curve([(0, 0, "A"), (1, 0.5, "B"), (2, 1, "C")], vdims=["y", "z"]),
            interpolation="steps-pre",
        )
        curve = hv.Curve(
            [(0, 0, "A"), (0, 0.5, "B"), (1, 0.5, "B"), (1, 1, "C"), (2, 1, "C")], vdims=["y", "z"]
        )
        assert_element_equal(interpolated, curve)

    def test_interpolate_datetime_curve_pre(self):
        dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)]).astype("M")
        values = [0, 1, 2, 3]
        interpolated = interpolate_curve(hv.Curve((dates, values)), interpolation="steps-pre")
        dates_interp = np.array(
            [
                "2017-01-01T00:00:00",
                "2017-01-01T00:00:00",
                "2017-01-02T00:00:00",
                "2017-01-02T00:00:00",
                "2017-01-03T00:00:00",
                "2017-01-03T00:00:00",
                "2017-01-04T00:00:00",
            ],
            dtype="datetime64[ns]",
        )
        curve = hv.Curve((dates_interp, [0, 1, 1, 2, 2, 3, 3]))
        assert_element_equal(interpolated, curve)

    def test_interpolate_curve_mid(self):
        interpolated = interpolate_curve(hv.Curve([0, 0.5, 1]), interpolation="steps-mid")
        curve = hv.Curve([(0, 0), (0.5, 0), (0.5, 0.5), (1.5, 0.5), (1.5, 1), (2, 1)])
        assert_element_equal(interpolated, curve)

    def test_interpolate_curve_mid_with_values(self):
        interpolated = interpolate_curve(
            hv.Curve([(0, 0, "A"), (1, 0.5, "B"), (2, 1, "C")], vdims=["y", "z"]),
            interpolation="steps-mid",
        )
        curve = hv.Curve(
            [
                (0, 0, "A"),
                (0.5, 0, "A"),
                (0.5, 0.5, "B"),
                (1.5, 0.5, "B"),
                (1.5, 1, "C"),
                (2, 1, "C"),
            ],
            vdims=["y", "z"],
        )
        assert_element_equal(interpolated, curve)

    def test_interpolate_datetime_curve_mid(self):
        dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)]).astype("M")
        values = [0, 1, 2, 3]
        interpolated = interpolate_curve(hv.Curve((dates, values)), interpolation="steps-mid")
        dates_interp = np.array(
            [
                "2017-01-01T00:00:00",
                "2017-01-01T12:00:00",
                "2017-01-01T12:00:00",
                "2017-01-02T12:00:00",
                "2017-01-02T12:00:00",
                "2017-01-03T12:00:00",
                "2017-01-03T12:00:00",
                "2017-01-04T00:00:00",
            ],
            dtype="datetime64[ns]",
        )
        curve = hv.Curve((dates_interp, [0, 0, 1, 1, 2, 2, 3, 3]))
        assert_element_equal(interpolated, curve)

    def test_interpolate_curve_post(self):
        interpolated = interpolate_curve(hv.Curve([0, 0.5, 1]), interpolation="steps-post")
        curve = hv.Curve([(0, 0), (1, 0), (1, 0.5), (2, 0.5), (2, 1)])
        assert_element_equal(interpolated, curve)

    def test_interpolate_curve_post_with_values(self):
        interpolated = interpolate_curve(
            hv.Curve([(0, 0, "A"), (1, 0.5, "B"), (2, 1, "C")], vdims=["y", "z"]),
            interpolation="steps-post",
        )
        curve = hv.Curve(
            [(0, 0, "A"), (1, 0, "A"), (1, 0.5, "B"), (2, 0.5, "B"), (2, 1, "C")], vdims=["y", "z"]
        )
        assert_element_equal(interpolated, curve)

    def test_interpolate_datetime_curve_post(self):
        dates = np.array([dt.datetime(2017, 1, i) for i in range(1, 5)]).astype("M")
        values = [0, 1, 2, 3]
        interpolated = interpolate_curve(hv.Curve((dates, values)), interpolation="steps-post")
        dates_interp = np.array(
            [
                "2017-01-01T00:00:00",
                "2017-01-02T00:00:00",
                "2017-01-02T00:00:00",
                "2017-01-03T00:00:00",
                "2017-01-03T00:00:00",
                "2017-01-04T00:00:00",
                "2017-01-04T00:00:00",
            ],
            dtype="datetime64[ns]",
        )
        curve = hv.Curve((dates_interp, [0, 0, 1, 1, 2, 2, 3]))
        assert_element_equal(interpolated, curve)

    @pytest.mark.parametrize("dtype", [pd.Int64Dtype(), np.int64])
    def test_interpolate_pandas_dtype_curve_post(self, dtype):
        df = pd.DataFrame(dict(a=[1, 2, 3], b=[3, 4, 5]), dtype=dtype)
        interpolated = interpolate_curve(hv.Curve(df), interpolation="steps-post")
        df = pd.DataFrame(dict(a=[1, 2, 2, 3, 3], b=[3, 3, 4, 4, 5]))
        expected = hv.Curve(df)
        assert_element_equal(interpolated, expected)

    def test_stack_area_overlay(self):
        areas = hv.Area([1, 2, 3]) * hv.Area([1, 2, 3])
        stacked = hv.Area.stack(areas)
        area1 = hv.Area(([0, 1, 2], [1, 2, 3], [0, 0, 0]), vdims=["y", "Baseline"])
        area2 = hv.Area(([0, 1, 2], [2, 4, 6], [1, 2, 3]), vdims=["y", "Baseline"])
        assert_element_equal(stacked, area1 * area2)

    def test_stack_area_ndoverlay(self):
        areas = hv.NdOverlay([(0, hv.Area([1, 2, 3])), (1, hv.Area([1, 2, 3]))])
        stacked = hv.Area.stack(areas)
        area1 = hv.Area(([0, 1, 2], [1, 2, 3], [0, 0, 0]), vdims=["y", "Baseline"])
        area2 = hv.Area(([0, 1, 2], [2, 4, 6], [1, 2, 3]), vdims=["y", "Baseline"])
        assert_element_equal(stacked, hv.NdOverlay([(0, area1), (1, area2)]))

    def test_pre_and_postprocess_hooks(self):
        pre_backup = operation._preprocess_hooks
        post_backup = operation._postprocess_hooks
        operation._preprocess_hooks = [lambda op, x: {"label": str(x.id)}]
        operation._postprocess_hooks = [lambda op, x, **kwargs: x.clone(**kwargs)]
        curve = hv.Curve([1, 2, 3])
        assert operation(curve).label == str(curve.id)
        operation._preprocess_hooks = pre_backup
        operation._postprocess_hooks = post_backup

    @pytest.mark.usefixtures("bokeh_backend")
    def test_decimate_ordering(self):
        x = np.linspace(0, 10, 100)
        y = np.sin(x)
        curve = hv.Curve((x, y))
        decimated = decimate(curve, max_samples=20)
        hv.renderer("bokeh").get_plot(decimated)

        index = decimated.data[()].data.index
        assert np.all(index == np.sort(index))

    @pytest.mark.usefixtures("bokeh_backend")
    def test_decimate_same_y(self):
        data = pd.DataFrame({"x": np.arange(10), "y": np.ones(10)})
        points = hv.Points(data, kdims=["x", "y"]).opts(xlim=(0, 10))
        decimated = decimate(points)

        hv.renderer("bokeh").get_plot(decimated)
        output = decimated.data[()].data
        pd.testing.assert_series_equal(data["x"], output["x"])
        pd.testing.assert_series_equal(data["y"], output["y"])


@pytest.mark.usefixtures("bokeh_backend")
class TestDendrogramOperation:
    def setup_class(self):
        pytest.importorskip("scipy")

        random.seed(1)
        self.df = pd.DataFrame(
            [(i, chr(65 + j), random.random()) for j in range(10) for i in range(5)],
            columns=["z", "x", "y"],
        )
        data = [
            [7, 8, 0, 0, 0, 0],
            [8, 6, 0, 0, 0, 0],
            [0, 0, 9, 8, 2, 7],
            [0, 0, 0, 1, 8, 2],
            [0, 7, 0, 7, 0, 0],
        ]
        self.df2 = pd.DataFrame(
            [
                {"cluster": f"clust {i}", "gene": f"gene {j}", "value": data[i][j]}
                for j in range(6)
                for i in range(5)
            ]
        )

        # Based on scanpy.datasets.pbmc68k_reduced
        obs = "abcdefghij"
        var = "mtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnkslmtropqnksl"
        # fmt: off
        data = [
            8, 10, 4, 8, 45, 6, 32, 45, 60, 6, 23, 10, 19, 0, 10, 6, 30, 7, 7, 6,
            8, 10, 16, 0, 10, 6, 7, 21, 7, 34, 8, 10, 23, 10, 10, 6, 7, 7, 7, 6,
            8, 10, 4, 5, 10, 6, 7, 19, 7, 31, 8, 10, 4, 4, 10, 22, 7, 7, 7, 6,
            8, 10, 4, 7, 10, 33, 7, 24, 22, 6, 8, 10, 4, 12, 10, 27, 7, 7, 7, 6,
            8, 10, 13, 25, 10, 6, 21, 7, 7, 6, 8, 10, 4, 7, 41, 6, 7, 40, 7, 23
        ]
        # fmt: on
        counts = [353, 491, 1185, 0, 1674, 2763, 710, 1613, 2377, 716]
        df = pd.DataFrame(
            {
                "obs": [x for x in obs for _ in range(10)],
                "var": list(var),
                "data": data,
                "counts": [x for x in counts for _ in range(10)],
            }
        )
        self.ds = hv.Dataset(df, kdims=["obs", "var"], vdims=["data", "counts"])

        self.bokeh_renderer = hv.renderer("bokeh")

    def get_childrens(self, adjoint):
        bk_childrens = self.bokeh_renderer.get_plot(adjoint).handles["plot"].children
        (atop, *_), (amain, *_), (aright, *_) = bk_childrens
        try:
            top = self.bokeh_renderer.get_plot(adjoint["top"]).handles["plot"]
        except SkipRendering:
            right = None
        main = self.bokeh_renderer.get_plot(adjoint["main"]).handles["plot"]
        try:
            right = self.bokeh_renderer.get_plot(adjoint["right"]).handles["plot"]
        except SkipRendering:
            right = None
        return (atop, amain, aright), (top, main, right)

    def test_right_only(self):
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["x"], main_dim="y")
        assert isinstance(dendro, hv.AdjointLayout)
        assert isinstance(dendro["main"], hv.HeatMap)
        assert isinstance(dendro["right"], hv.Dendrogram)
        assert isinstance(dendro["top"], hv.Empty)
        assert dendro["right"].kdims == ["__dendrogram_x_x", "__dendrogram_y_x"]

    def test_top_only(self):
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["z"], main_dim="y")
        assert isinstance(dendro, hv.AdjointLayout)
        assert isinstance(dendro["main"], hv.HeatMap)
        assert isinstance(dendro["right"], hv.Empty)
        assert isinstance(dendro["top"], hv.Dendrogram)
        assert dendro["top"].kdims == ["__dendrogram_x_z", "__dendrogram_y_z"]

    @pytest.mark.parametrize("adjoint_dims", [["x", "z"], ["z", "x"]], ids=["xz", "zx"])
    def test_both_xz(self, adjoint_dims):
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=adjoint_dims, main_dim="y")
        assert isinstance(dendro, hv.AdjointLayout)
        assert isinstance(dendro["main"], hv.HeatMap)
        assert isinstance(dendro["right"], hv.Dendrogram)
        assert isinstance(dendro["top"], hv.Dendrogram)
        assert dendro["right"].kdims == ["__dendrogram_x_x", "__dendrogram_y_x"]
        assert dendro["top"].kdims == ["__dendrogram_x_z", "__dendrogram_y_z"]

    def test_point_plot(self):
        dataset = hv.Points(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["x", "z"], main_dim="y")
        assert isinstance(dendro, hv.AdjointLayout)
        assert isinstance(dendro["main"], hv.Points)

    def test_depth_matches_non_adjoint(self):
        # depth dimensions is the orthogonal axis to the main plot
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["x", "z"], main_dim="y")
        (atop, amain, aright), (top, _main, right) = self.get_childrens(dendro)

        # Verify no shared axis is changing the depth dimension of the right
        assert atop.y_range.start == top.y_range.start
        assert atop.y_range.end == top.y_range.end
        assert atop.y_range.end == top.y_range.end

        # These should be shared with the main plot
        assert atop.x_range.start == amain.x_range.start
        assert atop.x_range.end == amain.x_range.end

        # Verify no shared axis is changing the depth dimension of the right
        assert aright.x_range.start == right.y_range.start
        assert aright.x_range.end == right.y_range.end

        # These should be shared with the main plot
        assert aright.y_range.factors == amain.y_range.factors

    def test_adjoned_False_1dim(self):
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["x"], main_dim="y", adjoined=False)

        assert isinstance(dendro, hv.Dendrogram)

    def test_adjoned_False_2dim(self):
        dataset = hv.Dataset(self.df)
        dendro = dendrogram(dataset, adjoint_dims=["x", "z"], main_dim="y", adjoined=False)

        assert isinstance(dendro, hv.Layout)
        assert len(dendro) == 2
        assert isinstance(dendro[0], hv.Dendrogram)
        assert isinstance(dendro[1], hv.Dendrogram)

    @pytest.mark.parametrize(
        "adjoint_dims",
        [["cluster"], ["gene"], ["gene", "cluster"]],
        ids=["right", "top", "both"],
    )
    def test_invert_dendrogram(self, adjoint_dims):
        plot = hv.Points(self.df2, kdims=["gene", "cluster"])
        dendro1 = dendrogram(plot, adjoint_dims=adjoint_dims, main_dim="value")
        dendro2 = dendrogram(plot, adjoint_dims=adjoint_dims, main_dim="value", invert=True)

        main1 = self.bokeh_renderer.get_plot(dendro1["main"]).handles["plot"]
        main2 = self.bokeh_renderer.get_plot(dendro2["main"]).handles["plot"]

        match adjoint_dims:
            case ["cluster"]:
                assert main1.x_range.factors == main2.x_range.factors
                assert main1.y_range.factors == main2.y_range.factors[::-1]
            case ["gene"]:
                assert main1.x_range.factors == main2.x_range.factors[::-1]
                assert main1.y_range.factors == main2.y_range.factors
            case _:
                assert main1.y_range.factors == main2.y_range.factors[::-1]
                assert main1.x_range.factors == main2.x_range.factors[::-1]

    @pytest.mark.parametrize(
        "adjoint_dims",
        [
            ["cluster"],
            ["gene"],
        ],
        ids=["right", "top"],
    )
    def test_assure_non_adjoined_axis_is_unchanged_points(self, adjoint_dims):
        # See: https://github.com/holoviz/holoviews/pull/6625#issuecomment-2981268665
        plot = hv.Points(self.df2, kdims=["gene", "cluster"])
        main1 = self.bokeh_renderer.get_plot(plot).handles["plot"]

        dendro = dendrogram(plot, adjoint_dims=adjoint_dims, main_dim="value")
        main2 = self.bokeh_renderer.get_plot(dendro["main"]).handles["plot"]

        match adjoint_dims:
            case ["cluster"]:
                assert main1.x_range.factors == main2.x_range.factors
            case ["gene"]:
                assert main1.y_range.factors == main2.y_range.factors

    def test_assure_non_adjoined_axis_is_unchanged_heatmap(self):
        # Follow up to previous test, see
        # https://github.com/holoviz/holoviews/pull/6669#issuecomment-3237153317
        plot = hv.HeatMap(self.ds)
        main1 = self.bokeh_renderer.get_plot(plot).handles["plot"]

        dendro = dendrogram(plot, adjoint_dims=["obs"], main_dim="data")
        main2 = self.bokeh_renderer.get_plot(dendro["main"]).handles["plot"]

        assert main1.y_range.factors == main2.y_range.factors

    @pytest.mark.parametrize(
        "adjoint_dims",
        [["cluster"], ["gene"], ["gene", "cluster"]],
        ids=["right", "top", "both"],
    )
    def test_gridded_dataset(self, adjoint_dims, rng):
        xr = pytest.importorskip("xarray")

        N = 10
        da = xr.DataArray(
            rng.normal(size=(N, N)),
            name="main",
            dims=("cluster", "gene"),
            coords={
                "cluster": [f"c{i}" for i in range(N)],
                "gene": [f"g{j}" for j in range(N)],
            },
        )

        dendro = dendrogram(hv.Dataset(da), adjoint_dims=adjoint_dims, main_dim="main")
        assert isinstance(dendro, hv.AdjointLayout)

    def test_failed_linkage(self):
        msg = "Could not calculate linkage for dendrogram"
        with pytest.raises(ValueError, match=msg):
            dendrogram(self.ds, adjoint_dims=["obs"], main_dim="counts")

        # This should work
        dendrogram(self.ds, adjoint_dims=["obs"], main_dim="counts", linkage_metric="euclidean")

    def test_not_primary_main_dim(self):
        # Adding hover to have access to the other dimension values
        plot = hv.HeatMap(self.ds).opts(tools=["hover"])
        assert plot.vdims[0] == "data"
        dendro = dendrogram(
            plot, adjoint_dims=["obs"], main_dim="counts", linkage_metric="euclidean"
        )
        (_, amain, _), *_ = self.get_childrens(dendro)
        data = amain.renderers[0].data_source.data
        assert list(data["zvalues"]) == list(map(int, data["data"]))


@pytest.mark.usefixtures("bokeh_backend")
def test_compositor_operations_size():
    # Operations are registered here:
    #  - holoviews.operations
    #  - holoviews.plotting, when changing backend
    #    we test the Bokeh backend here.

    # Update the count if more operations are added
    assert len(Compositor.operations) == 23

    # To verify we don't add a string instead of class
    for op in Compositor.operations:
        assert not isinstance(op, str)
