Repository: holoviz/holoviews

Issue:
Spans and Lines don’t support half-open hard ranges
#### ALL software version info

HoloViews: 1.18.1
Pandas: 2.1.4
NumPy: 1.26.3
Bokeh: 3.3.3
Matplotlib: 3.8.2
Python: 3.10.12

#### Description of error

The `*Spans` and `*Lines` annotations don’t support hard ranges unless both ends are specified.
Singular versions (`*Span` and `*Line`) are fine.

The issue seems due to converting extents to a Pandas array:
https://github.com/holoviz/holoviews/blob/e243fe080963956d4a1bc87aa6ccfad676cea671/holoviews/plotting/bokeh/annotation.py#L70-L82
which converts `nan` to `pandas.NA`. Then these lines filter out NaNs, but not `NA`s:
https://github.com/holoviz/holoviews/blob/e243fe080963956d4a1bc87aa6ccfad676cea671/holoviews/core/util.py#L1091-L1092

A search for `pd.array(extents)` shows this is done in both the **Bokeh** and **Matplotlib** renderers.
I also tested that the error occurs with both. I did not test Plotly.

#### Complete, minimal, self-contained example code that reproduces the issue

The following will raise the exception printed above

```python
import holoviews as hv
hv.extension("bokeh")

dim_prob = hv.Dimension('p', label="prob", range=(0, None))
fig = hv.Curve([(0, 0.6), (1, 0.3), (2, 0.4), (3, 0.45)], kdims="x", vdims=dim_prob) \
      * hv.HSpans([(0, .2), (0.4, 0.6)], kdims=["x", dim_prob])
display(fig)   # Error occurs when the figure is displayed
```
With any of the following changes, the example _does_ work:

- `range=(0, 1)` instead of `range=(0, None)`
- `soft_range=(0, None)` instead of `range=(0, None)`
- `HSpan` instead of `HSpans`

#### Possible workaround

Use `soft_range` for half-open ranges.

#### Stack traceback and/or browser JavaScript console output

Last bit of traceback:

```
...
File [~/usr/venv/comp/lib/python3.10/site-packages/holoviews/plotting/plot.py:1988](http://localhost:33091/home/rene/usr/venv/comp/lib/python3.10/site-packages/holoviews/plotting/plot.py#line=1987), in GenericOverlayPlot.get_extents(self, overlay, ranges, range_type, dimension, **kwargs)
   1986 subplot_extents = self._get_subplot_extents(overlay, ranges, range_type, dimension)
   1987 zrange = isinstance(self.projection, str) and self.projection == '3d'
-> 1988 extents = {k: util.max_extents(rs, zrange) for k, rs in subplot_extents.items()}
   1989 if range_type != 'combined':
   1990     return extents[range_type]

File [~/usr/venv/comp/lib/python3.10/site-packages/holoviews/plotting/plot.py:1988](http://localhost:33091/home/rene/usr/venv/comp/lib/python3.10/site-packages/holoviews/plotting/plot.py#line=1987), in <dictcomp>(.0)
   1986 subplot_extents = self._get_subplot_extents(overlay, ranges, range_type, dimension)
   1987 zrange = isinstance(self.projection, str) and self.projection == '3d'
-> 1988 extents = {k: util.max_extents(rs, zrange) for k, rs in subplot_extents.items()}
   1989 if range_type != 'combined':
   1990     return extents[range_type]

File [~/usr/venv/comp/lib/python3.10/site-packages/holoviews/core/util.py:1121](http://localhost:33091/home/rene/usr/venv/comp/lib/python3.10/site-packages/holoviews/core/util.py#line=1120), in max_extents(extents, zrange)
   1119 for lidx, uidx in inds:
   1120     lower = [v for v in arr[lidx] if v is not None and not is_nan(v)]
-> 1121     upper = [v for v in arr[uidx] if v is not None and not is_nan(v)]
   1122     if lower and isinstance(lower[0], datetime_types):
   1123         extents[lidx] = np.min(lower)

File [~/usr/venv/comp/lib/python3.10/site-packages/holoviews/core/util.py:1121](http://localhost:33091/home/rene/usr/venv/comp/lib/python3.10/site-packages/holoviews/core/util.py#line=1120), in <listcomp>(.0)
   1119 for lidx, uidx in inds:
   1120     lower = [v for v in arr[lidx] if v is not None and not is_nan(v)]
-> 1121     upper = [v for v in arr[uidx] if v is not None and not is_nan(v)]
   1122     if lower and isinstance(lower[0], datetime_types):
   1123         extents[lidx] = np.min(lower)

File missing.pyx:419, in pandas._libs.missing.NAType.__bool__()

TypeError: boolean value of NA is ambiguous
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6290?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.55%. Comparing base [(`e243fe0`)](https://app.codecov.io/gh/holoviz/holoviews/commit/e243fe080963956d4a1bc87aa6ccfad676cea671?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`029814b`)](https://app.codecov.io/gh/holoviz/holoviews/commit/029814bd0621c4006e369f8e13f8383afef687b5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6290      +/-   ##
==========================================
- Coverage   88.55%   88.55%   -0.01%     
==========================================
  Files         323      323              
  Lines       68000    68007       +7     
==========================================
+ Hits        60218    60223       +5     
- Misses       7782     7784       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6290?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b07bbc720..75b25b119 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,7 +20,7 @@ repos:
       - id: check-json
       - id: detect-private-key
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.4.9
+    rev: v0.4.10
     hooks:
       - id: ruff
         files: holoviews/|scripts/
diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css
index a99250e44..3763163ef 100644
--- a/doc/_static/css/custom.css
+++ b/doc/_static/css/custom.css
@@ -6,6 +6,7 @@
   --pst-color-primary: var(--holoviz-main-color);
   --pst-color-secondary-highlight: var(--holoviz-main-color);
   --pst-color-secondary: var(--holoviz-main-color);
+  --pst-violet-600: var(--holoviz-main-color); /* back to top hover */
   --sd-color-card-border-hover: var(--holoviz-main-color);
 }
 
diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py
index a097bdf95..300391cbf 100644
--- a/holoviews/core/data/pandas.py
+++ b/holoviews/core/data/pandas.py
@@ -72,7 +72,7 @@ class PandasInterface(Interface, PandasAPI):
 
             if kdims and not (len(kdims) == len(index_names) and {dimension_name(kd) for kd in kdims} == set(index_names)):
                 kdim = dimension_name(kdims[0])
-                if eltype._auto_indexable_1d and ncols == 1 and kdim not in data.columns:
+                if eltype._auto_indexable_1d and ncols == 1 and kdim not in [*data.columns, *cls.indexes(data)]:
                     data = data.copy()
                     data.insert(0, kdim, np.arange(len(data)))
 
@@ -256,8 +256,12 @@ class PandasInterface(Interface, PandasAPI):
             # when iterating over a groupby with a grouper equal to a list of length 1.
             # Don't supply a list with a single grouper to avoid this warning.
             group_by = group_by[0]
+        if util.pandas_version >= Version("2.1.0"):
+            groupby_kwargs = {"sort": False, "observed": False}
+        else:
+            groupby_kwargs = {"sort": False}
         data = [(k, group_type(v, **group_kwargs)) for k, v in
-                dataset.data.groupby(group_by, sort=False)]
+                dataset.data.groupby(group_by, **groupby_kwargs)]
         if issubclass(container_type, NdMapping):
             with item_check(False), sorted_context(False):
                 return container_type(data, kdims=index_dims)
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index dadba0c08..4b91e612b 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -2031,7 +2031,9 @@ def is_nan(x):
     Checks whether value is NaN on arbitrary types
     """
     try:
-        return np.isnan(x)
+        # Using pd.isna instead of np.isnan as np.isnan(pd.NA) returns pd.NA!
+        # Call bool() to raise an error if x is pd.NA, an array, etc.
+        return bool(pd.isna(x))
     except Exception:
         return False
 
diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py
index 82698dbf7..a80bf1e4e 100644
--- a/holoviews/plotting/bokeh/callbacks.py
+++ b/holoviews/plotting/bokeh/callbacks.py
@@ -613,7 +613,8 @@ class PopupMixin:
             position=XY(x=np.nan, y=np.nan),
             anchor="top_left",
             elements=[close_button],
-            visible=False
+            visible=False,
+            styles={"zIndex": "1000"},
         )
         close_button.js_on_click(CustomJS(args=dict(panel=self._panel), code="panel.visible = false"))
 
diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py
index 0bbd705ad..17ef69983 100644
--- a/holoviews/plotting/bokeh/chart.py
+++ b/holoviews/plotting/bokeh/chart.py
@@ -922,7 +922,8 @@ class BarPlot(BarsMixin, ColorbarPlot, LegendPlot):
             is_dt = isdatetime(xvals)
             if is_dt or xvals.dtype.kind not in 'OU':
                 xdiff = np.abs(np.diff(xvals))
-                if len(np.unique(xdiff)) == 1 and xdiff[0] == 0:
+                diff_size = len(np.unique(xdiff))
+                if diff_size == 0 or (diff_size == 1 and xdiff[0] == 0):
                     xdiff = 1
                 if is_dt:
                     width *= xdiff.astype('timedelta64[ms]').astype(np.int64)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 659e4bd52..8c5e4c32c 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -2864,7 +2864,7 @@ class ColorbarPlot(ElementPlot):
             opts.update({opt: colors[name] for name, opt in color_opts if name in colors})
         else:
             colormapper = CategoricalColorMapper
-            factors = decode_bytes(factors)
+            factors = map(str, decode_bytes(factors))
             opts = dict(factors=list(factors))
             if 'NaN' in colors:
                 opts['nan_color'] = colors['NaN']
diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py
index 879fd260e..5da620b85 100644
--- a/holoviews/plotting/mpl/chart.py
+++ b/holoviews/plotting/mpl/chart.py
@@ -1,7 +1,6 @@
 import matplotlib as mpl
 import numpy as np
 import param
-from matplotlib import cm
 from matplotlib.collections import LineCollection
 from matplotlib.dates import DateFormatter, date2num
 from packaging.version import Version
@@ -27,7 +26,7 @@ from ..util import compute_sizes, dim_range_key, get_min_distance, get_sideplot_
 from .element import ColorbarPlot, ElementPlot, LegendPlot
 from .path import PathPlot
 from .plot import AdjoinedPlot, mpl_rc_context
-from .util import mpl_version
+from .util import MPL_GE_3_7, MPL_GE_3_9, mpl_version
 
 
 class ChartPlot(ElementPlot):
@@ -95,7 +94,10 @@ class CurvePlot(ChartPlot):
     def init_artists(self, ax, plot_args, plot_kwargs):
         xs, ys = plot_args
         if isdatetime(xs):
-            artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0]
+            if MPL_GE_3_9:
+                artist = ax.plot(xs, ys, '-', **plot_kwargs)[0]
+            else:
+                artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0]
         else:
             artist = ax.plot(xs, ys, **plot_kwargs)[0]
         return {'artist': artist}
@@ -501,7 +503,12 @@ class SideHistogramPlot(AdjoinedPlot, HistogramPlot):
         # Get colormapping options
         if isinstance(range_item, (HeatMap, Raster)) or (cdim and cdim in element):
             style = self.lookup_options(range_item, 'style')[self.cyclic_index]
-            cmap = cm.get_cmap(style.get('cmap'))
+            if MPL_GE_3_7:
+                # https://github.com/matplotlib/matplotlib/pull/28355
+                cmap = mpl.colormaps.get_cmap(style.get('cmap'))
+            else:
+                from matplotlib import cm
+                cmap = cm.get_cmap(style.get('cmap'))
             main_range = style.get('clims', main_range)
         else:
             cmap = None
diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py
index f728404c3..9764de4e1 100644
--- a/holoviews/plotting/mpl/stats.py
+++ b/holoviews/plotting/mpl/stats.py
@@ -6,6 +6,7 @@ from ..mixins import MultiDistributionMixin
 from .chart import AreaPlot, ChartPlot
 from .path import PolygonPlot
 from .plot import AdjoinedPlot
+from .util import MPL_GE_3_9
 
 
 class DistributionPlot(AreaPlot):
@@ -77,7 +78,10 @@ class BoxPlot(MultiDistributionMixin, ChartPlot):
             d = group[group.vdims[0]]
             data.append(d[np.isfinite(d)])
             labels.append(label)
-        style['labels'] = labels
+        if MPL_GE_3_9:
+            style['tick_labels'] = labels
+        else:
+            style['labels'] = labels
         style = {k: v for k, v in style.items()
                  if k not in ['zorder', 'label']}
         style['vert'] = not self.invert_axes
@@ -157,7 +161,10 @@ class ViolinPlot(BoxPlot):
         stats_color = plot_kwargs.pop('stats_color', 'black')
         facecolors = plot_kwargs.pop('facecolors', [])
         edgecolors = plot_kwargs.pop('edgecolors', 'black')
-        labels = plot_kwargs.pop('labels')
+        if MPL_GE_3_9:
+            labels = {'tick_labels': plot_kwargs.pop('tick_labels')}
+        else:
+            labels = {'labels': plot_kwargs.pop('labels')}
         alpha = plot_kwargs.pop('alpha', 1.)
         showmedians = self.inner == 'medians'
         bw_method = self.bandwidth or 'scott'
@@ -168,7 +175,7 @@ class ViolinPlot(BoxPlot):
                              showfliers=False, showcaps=False, patch_artist=True,
                              boxprops={'facecolor': box_color},
                              medianprops={'color': 'white'}, widths=0.1,
-                             labels=labels)
+                             **labels)
             artists.update(box)
         for body, color in zip(artists['bodies'], facecolors):
             body.set_facecolors(color)
@@ -199,7 +206,10 @@ class ViolinPlot(BoxPlot):
             labels.append(label)
             colors.append(elstyle[i].get('facecolors', 'blue'))
         style['positions'] = list(range(len(data)))
-        style['labels'] = labels
+        if MPL_GE_3_9:
+            style['tick_labels'] = labels
+        else:
+            style['labels'] = labels
         style['facecolors'] = colors
 
         if element.ndims > 0:
diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py
index 4699d8590..99c5a9b37 100644
--- a/holoviews/plotting/mpl/util.py
+++ b/holoviews/plotting/mpl/util.py
@@ -37,6 +37,8 @@ from ...element import RGB, Polygons, Raster
 from ..util import COLOR_ALIASES, RGB_HEX_REGEX
 
 mpl_version = Version(mpl.__version__)
+MPL_GE_3_7 = mpl_version >= Version('3.7')
+MPL_GE_3_9 = mpl_version >= Version('3.9')
 
 
 def is_color(color):
diff --git a/holoviews/util/_versions.py b/holoviews/util/_versions.py
index 731dff221..e6f45c5df 100644
--- a/holoviews/util/_versions.py
+++ b/holoviews/util/_versions.py
@@ -7,7 +7,9 @@ __all__ = ("show_versions",)
 PACKAGES = [
     # Data
     "cudf",
+    "cupy",
     "dask",
+    "dask-expr",
     "ibis-framework",
     "networkx",
     "numpy",
@@ -33,6 +35,7 @@ PACKAGES = [
     # Jupyter
     "IPython",
     "jupyter_bokeh",
+    "ipywidgets_bokeh",
     "jupyterlab",
     "notebook",
     # Misc
@@ -55,9 +58,9 @@ def show_versions():
 
 def _package_version(p):
     try:
-        print(f"{p:20}:  {version(p)}")
+        print(f"{p.replace('_', '-'):20}:  {version(p)}")
     except ImportError:
-        print(f"{p:20}:  -")
+        print(f"{p.replace('_', '-'):20}:  -")
 
 
 def _panel_comms():
diff --git a/pixi.toml b/pixi.toml
index c8c272798..b0313eddd 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -19,7 +19,6 @@ test-311 = ["py311", "test-core", "test", "example", "test-example", "test-unit-
 test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit-task"]
 test-ui = ["py312", "test-core", "test", "test-ui"]
 test-core = ["py312", "test-core", "test-unit-task"]
-test-numpy = ["py312", "test-core", "test-unit-task", "numpy2", "test-example"]
 test-gpu = ["py311", "test-core", "test", "test-gpu"]
 docs = ["py311", "example", "doc"]
 build = ["py311", "build"]
@@ -44,12 +43,15 @@ python = "3.9.*"
 
 [feature.py310.dependencies]
 python = "3.10.*"
+bokeh_sampledata = "*"
 
 [feature.py311.dependencies]
 python = "3.11.*"
+bokeh_sampledata = "*"
 
 [feature.py312.dependencies]
 python = "3.12.*"
+bokeh_sampledata = "*"
 
 [feature.example.dependencies]
 cftime = "*"
@@ -142,39 +144,6 @@ rmm = { version = "*", channel = "rapidsai" }
 [feature.test-gpu.tasks]
 test-gpu = "pytest holoviews/tests --gpu"
 
-[feature.numpy2]
-channels = ["pyviz/label/dev", "conda-forge/label/numpy_rc", "numba/label/dev", "conda-forge"]
-
-[feature.numpy2.dependencies]
-numpy = "2.*"
-numba = { version = "0.60.*", channel = "numba/label/dev" }
-llvmlite = { version = "*", channel = "numba/label/dev" }
-
-# test dependencies
-cftime = "*"
-contourpy = "*"
-dask-core = "*"
-datashader = ">=0.11.1"
-ffmpeg = "*"
-ibis-sqlite = "*"
-nbconvert = "*"
-networkx = "*"
-pillow = "*"
-scipy = ">=1.10" # Python 3.9 + Windows downloads 1.9
-selenium = "*"
-shapely = "*"
-# spatialpandas = "*"
-xarray = ">=0.10.4"
-xyzservices = "*"
-
-# Examples (removed duplicates)
-# netcdf4 = "*"
-notebook = "*"
-pooch = "*"
-# pyarrow = "*"
-# scikit-image = "*"
-streamz = ">=0.5.0"
-
 # =============================================
 # =================== DOCS ====================
 # =============================================
diff --git a/pyproject.toml b/pyproject.toml
index f14745939..a95d6e4d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,12 +54,13 @@ holoviews = "holoviews.util.command:main"
 
 [tool.hatch.version]
 source = "vcs"
+raw-options = { version_scheme = "no-guess-dev" }
 
 [tool.hatch.build.targets.wheel]
 include = ["holoviews"]
 
 [tool.hatch.build.targets.sdist]
-include = ["holoviews", "CHANGELOG.md"]
+include = ["holoviews", "scripts"]
 
 [tool.hatch.build.targets.sdist.force-include]
 examples = "holoviews/examples"
@@ -118,6 +119,8 @@ filterwarnings = [
     # 2024-06
     "ignore:\\s*Dask dataframe query planning is disabled because dask-expr is not installed:FutureWarning", # OK
     "ignore:unclosed file <_io.TextIOWrapper name='/dev/null' mode='w':ResourceWarning", # OK
+    # 2024-07
+    "ignore:The (non_)?interactive_bk attribute was deprecated in Matplotlib 3.9", # OK - Only happening in debug mode
 ]
 
 [tool.coverage]
diff --git a/scripts/download_data.py b/scripts/download_data.py
index cb987a402..00c900eab 100644
--- a/scripts/download_data.py
+++ b/scripts/download_data.py
@@ -1,8 +1,12 @@
 from contextlib import suppress
 
-import bokeh.sampledata
+import bokeh
+from packaging.version import Version
 
-bokeh.sampledata.download()
+if Version(Version(bokeh.__version__).base_version) < Version("3.5"):
+    import bokeh.sampledata
+
+    bokeh.sampledata.download()
 
 with suppress(ImportError):
     import pooch  # noqa: F401


Test patch:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index d18cf85c8..2f5e4f208 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -83,10 +83,7 @@ jobs:
         run: |
           MATRIX=$(jq -nsc '{
               "os": ["ubuntu-latest", "macos-latest", "windows-latest"],
-              "environment": ["test-39", "test-312"],
-              "include": [
-                { "os": "ubuntu-latest", "environment": "test-numpy" }
-              ]
+              "environment": ["test-39", "test-312"]
           }')
           echo "MATRIX=$MATRIX" >> $GITHUB_ENV
       - name: Set test matrix with 'full' option
@@ -94,10 +91,7 @@ jobs:
         run: |
           MATRIX=$(jq -nsc '{
               "os": ["ubuntu-latest", "macos-latest", "windows-latest"],
-              "environment": ["test-39", "test-310", "test-311", "test-312"],
-              "include": [
-                { "os": "ubuntu-latest", "environment": "test-numpy" }
-              ]
+              "environment": ["test-39", "test-310", "test-311", "test-312"]
           }')
           echo "MATRIX=$MATRIX" >> $GITHUB_ENV
       - name: Set test matrix with 'downstream' option
diff --git a/holoviews/tests/core/data/test_daskinterface.py b/holoviews/tests/core/data/test_daskinterface.py
index 84ab40035..05aa2661d 100644
--- a/holoviews/tests/core/data/test_daskinterface.py
+++ b/holoviews/tests/core/data/test_daskinterface.py
@@ -3,7 +3,6 @@ from unittest import SkipTest
 
 import numpy as np
 import pandas as pd
-import pytest
 from packaging.version import Version
 
 try:
@@ -166,7 +165,3 @@ class DaskExprDatasetTest(_DaskDatasetTest):
     def test_dataset_groupby_alias(self):
         # Dask-expr unique sort the order when running unique on column
         super().test_dataset_groupby_alias(sort=True)
-
-    @pytest.mark.xfail(reason="Not supported yet, see https://github.com/dask/dask-expr/issues/1076")
-    def test_multi_dimension_groupby(self):
-        super().test_multi_dimension_groupby()
diff --git a/holoviews/tests/core/data/test_pandasinterface.py b/holoviews/tests/core/data/test_pandasinterface.py
index bb2e520d7..1c4022922 100644
--- a/holoviews/tests/core/data/test_pandasinterface.py
+++ b/holoviews/tests/core/data/test_pandasinterface.py
@@ -184,6 +184,13 @@ class PandasInterfaceTests(BasePandasInterfaceTests):
         data = Dataset(df).dimension_values("dates")
         np.testing.assert_equal(dates, data)
 
+    def test_data_groupby_categorial(self):
+        # Test for https://github.com/holoviz/holoviews/issues/6305
+        df = pd.DataFrame({"y": [1, 2], "by": ["A", "B"]})
+        df["by"] = pd.Categorical(df["by"])
+        ds = Dataset(df, kdims="index", vdims="y").to(Scatter, groupby="by")
+        assert ds.keys() == ["A", "B"]
+
     @pytest.mark.xfail(reason="Breaks hvplot")
     def test_reindex(self):
         ds = Dataset(pd.DataFrame({'x': np.arange(10), 'y': np.arange(10), 'z': np.random.rand(10)}))
@@ -391,3 +398,9 @@ class PandasInterfaceMultiIndex(HeterogeneousColumnTests, InterfaceTests):
         assert list(grouped.keys()) == [0, 1, 2, 3]
         for k, v in grouped.items():
             pd.testing.assert_frame_equal(v.data, ds.select(values=k).data)
+
+    def test_regression_no_auto_index(self):
+        # https://github.com/holoviz/holoviews/issues/6298
+
+        plot = Scatter(self.df, kdims="number")
+        np.testing.assert_equal(plot.dimension_values('number'), self.df.index.get_level_values('number'))
diff --git a/holoviews/tests/core/test_utils.py b/holoviews/tests/core/test_utils.py
index 560f11245..41462601d 100644
--- a/holoviews/tests/core/test_utils.py
+++ b/holoviews/tests/core/test_utils.py
@@ -21,6 +21,7 @@ from holoviews.core.util import (
     dt_to_int,
     find_range,
     get_path,
+    is_nan,
     isfinite,
     make_path_unique,
     max_range,
@@ -806,3 +807,13 @@ def test_unique_array_categorial():
     ser = pd.Series(np.random.choice(["a", "b", "c"], 100)).astype("category")
     res = unique_array([ser])
     assert sorted(res) == ["a", "b", "c"]
+
+
+def test_is_nan():
+    assert is_nan(np.nan) == True
+    assert is_nan(None) == True
+    assert is_nan(pd.NA) == True
+    assert is_nan(pd.NaT) == True
+    assert is_nan([1, 1]) == False
+    assert is_nan([np.nan]) == True
+    assert is_nan([np.nan, np.nan]) == False
diff --git a/holoviews/tests/plotting/bokeh/test_annotationplot.py b/holoviews/tests/plotting/bokeh/test_annotationplot.py
index 4a60a3e82..4520435b2 100644
--- a/holoviews/tests/plotting/bokeh/test_annotationplot.py
+++ b/holoviews/tests/plotting/bokeh/test_annotationplot.py
@@ -621,3 +621,15 @@ class TestHVSpansPlot(TestBokehPlot):
         assert plot_el.handles["x_range"].end == plot_dmap.handles["x_range"].end
         assert plot_el.handles["y_range"].start == plot_dmap.handles["y_range"].start
         assert plot_el.handles["y_range"].end == plot_dmap.handles["y_range"].end
+
+    def test_hspans_no_upper_range(self):
+        # Test for: https://github.com/holoviz/holoviews/issues/6289
+
+        dim = hv.Dimension("p", label="prob", range=(0, None))
+        fig = hv.Curve(
+            [(0, 0.6), (1, 0.3), (2, 0.4), (3, 0.45)], kdims="x", vdims=dim
+        )
+        spans = hv.HSpans([(0, 0.2), (0.4, 0.6)], kdims=["x", dim])
+        plot_el = bokeh_renderer.get_plot(spans * fig)
+        assert plot_el.handles["x_range"].start == 0
+        assert plot_el.handles["x_range"].end == 3
diff --git a/holoviews/tests/plotting/bokeh/test_barplot.py b/holoviews/tests/plotting/bokeh/test_barplot.py
index fd16dbd7b..272b32103 100644
--- a/holoviews/tests/plotting/bokeh/test_barplot.py
+++ b/holoviews/tests/plotting/bokeh/test_barplot.py
@@ -37,6 +37,14 @@ class TestBarPlot(TestBokehPlot):
         for v in source.data.values():
             self.assertEqual(len(v), 0)
 
+    def test_bars_single_value(self):
+        df = pd.DataFrame({"time": [1], "value": [-1]})
+        bars = Bars(df)
+        plot = bokeh_renderer.get_plot(bars)
+        source = plot.handles['source']
+        assert source.data['time'], np.array([1])
+        assert source.data['value'], np.array([-1])
+
     def test_bars_grouped_categories(self):
         bars = Bars([('A', 0, 1), ('A', 1, -1), ('B', 0, 2)],
                     kdims=['Index', 'Category'], vdims=['Value'])
@@ -159,6 +167,13 @@ class TestBarPlot(TestBokehPlot):
         self.assertEqual(y_range.start, 0.01)
         self.assertEqual(y_range.end, 3.3483695221017129)
 
+    def test_bars_boolean_kdims(self):
+        data = pd.DataFrame({"x1": [1, 1, 2, 2], "x2": [False, True, False, True], "y": [3, 1, 2, 2]})
+        bars = Bars(data, kdims=["x1", "x2"])
+        plot = bokeh_renderer.get_plot(bars)
+        x_range = plot.handles['x_range']
+        assert x_range.factors == [('1', 'False'), ('1', 'True'), ('2', 'False'), ('2', 'True')]
+
     ###########################
     #    Styling mapping      #
     ###########################
diff --git a/holoviews/tests/plotting/matplotlib/test_annotationplot.py b/holoviews/tests/plotting/matplotlib/test_annotationplot.py
index d1c188449..acd795994 100644
--- a/holoviews/tests/plotting/matplotlib/test_annotationplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_annotationplot.py
@@ -2,6 +2,7 @@ import numpy as np
 
 import holoviews as hv
 from holoviews.element import HLines, HSpans, VLines, VSpans
+from holoviews.plotting.mpl.util import MPL_GE_3_9
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -153,6 +154,25 @@ class TestHVLinesPlot(TestMPLPlot):
 
 
 class TestHVSpansPlot(TestMPLPlot):
+
+    def _hspans_check(self, source, v0, v1):
+        # Matplotlib 3.9+ uses a rectangle instead of polygon
+        if MPL_GE_3_9:
+            rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()]
+            assert np.allclose(rect, [0, v0, 1, v1 - v0])
+        else:
+            assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0])
+            assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0])
+
+    def _vspans_check(self, source, v0, v1):
+        # Matplotlib 3.9+ uses a rectangle instead of polygon
+        if MPL_GE_3_9:
+            rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()]
+            assert np.allclose(rect, [v0, 0, v1 - v0, 1])
+        else:
+            assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0])
+            assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0])
+
     def test_hspans_plot(self):
         hspans = HSpans(
             {"y0": [0, 3, 5.5], "y1": [1, 4, 6.5], "extra": [-1, -2, -3]},
@@ -164,14 +184,17 @@ class TestHVSpansPlot(TestMPLPlot):
 
         xlim = plot.handles["fig"].axes[0].get_xlim()
         ylim = plot.handles["fig"].axes[0].get_ylim()
-        assert np.allclose(xlim, (-0.055, 0.055))
+        if MPL_GE_3_9:
+            assert np.allclose(xlim, (-0.05, 1.05))
+        else:
+            assert np.allclose(xlim, (-0.055, 0.055))
+
         assert np.allclose(ylim, (0, 6.5))
 
         sources = plot.handles["annotations"]
         assert len(sources) == 3
         for source, v0, v1 in zip(sources, hspans.data["y0"], hspans.data["y1"]):
-            assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0])
-            assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0])
+            self._hspans_check(source, v0, v1)
 
     def test_hspans_inverse_plot(self):
         hspans = HSpans(
@@ -190,8 +213,7 @@ class TestHVSpansPlot(TestMPLPlot):
         sources = plot.handles["annotations"]
         assert len(sources) == 3
         for source, v0, v1 in zip(sources, hspans.data["y0"], hspans.data["y1"]):
-            assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0])
-            assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0])
+            self._vspans_check(source, v0, v1)
 
     def test_dynamicmap_overlay_hspans(self):
         el = HSpans(data=[[1, 3], [2, 4]])
@@ -218,7 +240,10 @@ class TestHVSpansPlot(TestMPLPlot):
 
         xlim = plot.handles["fig"].axes[0].get_xlim()
         ylim = plot.handles["fig"].axes[0].get_ylim()
-        assert np.allclose(xlim, (-0.055, 0.055))
+        if MPL_GE_3_9:
+            assert np.allclose(xlim, (-0.05, 1.05))
+        else:
+            assert np.allclose(xlim, (-0.055, 0.055))
         assert np.allclose(ylim, (0, 6.5))
 
         sources = plot.handles["annotations"]
@@ -226,8 +251,7 @@ class TestHVSpansPlot(TestMPLPlot):
         for source, v0, v1 in zip(
             sources, hspans.data["other0"], hspans.data["other1"]
         ):
-            assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0])
-            assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0])
+            self._hspans_check(source, v0, v1)
 
     def test_vspans_plot(self):
         vspans = VSpans(
@@ -246,8 +270,7 @@ class TestHVSpansPlot(TestMPLPlot):
         sources = plot.handles["annotations"]
         assert len(sources) == 3
         for source, v0, v1 in zip(sources, vspans.data["x0"], vspans.data["x1"]):
-            assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0])
-            assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0])
+            self._vspans_check(source, v0, v1)
 
     def test_vspans_inverse_plot(self):
         vspans = VSpans(
@@ -260,14 +283,16 @@ class TestHVSpansPlot(TestMPLPlot):
 
         xlim = plot.handles["fig"].axes[0].get_xlim()
         ylim = plot.handles["fig"].axes[0].get_ylim()
-        assert np.allclose(xlim, (-0.055, 0.055))
+        if MPL_GE_3_9:
+            assert np.allclose(xlim, (-0.05, 1.05))
+        else:
+            assert np.allclose(xlim, (-0.055, 0.055))
         assert np.allclose(ylim, (0, 6.5))
 
         sources = plot.handles["annotations"]
         assert len(sources) == 3
         for source, v0, v1 in zip(sources, vspans.data["x0"], vspans.data["x1"]):
-            assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0])
-            assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0])
+            self._hspans_check(source, v0, v1)
 
     def test_vspans_nondefault_kdims(self):
         vspans = VSpans(
@@ -287,8 +312,7 @@ class TestHVSpansPlot(TestMPLPlot):
         for source, v0, v1 in zip(
             sources, vspans.data["other0"], vspans.data["other1"]
         ):
-            assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0])
-            assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0])
+            self._vspans_check(source, v0, v1)
 
     def test_vspans_hspans_overlay(self):
         hspans = HSpans(
@@ -310,12 +334,10 @@ class TestHVSpansPlot(TestMPLPlot):
 
         sources = plot.handles["fig"].axes[0].get_children()
         for source, v0, v1 in zip(sources[:3], hspans.data["y0"], hspans.data["y1"]):
-            assert np.allclose(source.xy[:, 0], [0, 0, 1, 1, 0])
-            assert np.allclose(source.xy[:, 1], [v0, v1, v1, v0, v0])
+            self._hspans_check(source, v0, v1)
 
         for source, v0, v1 in zip(sources[3:6], vspans.data["x0"], vspans.data["x1"]):
-            assert np.allclose(source.xy[:, 1], [0, 1, 1, 0, 0])
-            assert np.allclose(source.xy[:, 0], [v0, v0, v1, v1, v0])
+            self._vspans_check(source, v0, v1)
 
     def test_dynamicmap_overlay_vspans(self):
         el = VSpans(data=[[1, 3], [2, 4]])
diff --git a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
index cf28d04ab..2c2ec926b 100644
--- a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
+++ b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
@@ -1,6 +1,7 @@
 import numpy as np
 
 from holoviews.element import BoxWhisker
+from holoviews.plotting.mpl.util import MPL_GE_3_9
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -13,7 +14,10 @@ class TestMPLBoxWhiskerPlot(TestMPLPlot):
         plot = mpl_renderer.get_plot(boxwhisker)
         data, style, axis_opts = plot.get_data(boxwhisker, {}, {})
         self.assertEqual(data[0][0], values)
-        self.assertEqual(style['labels'], [''])
+        if MPL_GE_3_9:
+            self.assertEqual(style['tick_labels'], [''])
+        else:
+            self.assertEqual(style['labels'], [''])
 
     def test_boxwhisker_simple_overlay(self):
         values = np.random.rand(100)
diff --git a/holoviews/tests/plotting/matplotlib/test_violinplot.py b/holoviews/tests/plotting/matplotlib/test_violinplot.py
index 8a1addf25..e87fdb539 100644
--- a/holoviews/tests/plotting/matplotlib/test_violinplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_violinplot.py
@@ -1,6 +1,7 @@
 import numpy as np
 
 from holoviews.element import Violin
+from holoviews.plotting.mpl.util import MPL_GE_3_9
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -14,7 +15,10 @@ class TestMPLViolinPlot(TestMPLPlot):
         data, style, axis_opts = plot.get_data(violin, {}, {})
         self.assertEqual(data[0][0], values)
         self.assertEqual(style['positions'], [0])
-        self.assertEqual(style['labels'], [''])
+        if MPL_GE_3_9:
+            self.assertEqual(style['tick_labels'], [''])
+        else:
+            self.assertEqual(style['labels'], [''])
 
     def test_violin_simple_overlay(self):
         values = np.random.rand(100)
@@ -34,4 +38,7 @@ class TestMPLViolinPlot(TestMPLPlot):
         self.assertEqual(data[0][0], violin.select(A=0).dimension_values(1))
         self.assertEqual(data[0][1], violin.select(A=1).dimension_values(1))
         self.assertEqual(style['positions'], [0, 1])
-        self.assertEqual(style['labels'], ['0', '1'])
+        if MPL_GE_3_9:
+            self.assertEqual(style['tick_labels'], ['0', '1'])
+        else:
+            self.assertEqual(style['labels'], ['0', '1'])