Repository: holoviz/holoviews
Pull request #6296 (2024-06-28 10:32:10)

Update ruff to 0.5.0
``` bash
❯ ruff check holoviews --output-format=concise --no-fix
holoviews/annotators.py:132:13: PLR1704 Redefining argument with the local name `element`
holoviews/core/accessors.py:521:13: PLR1704 Redefining argument with the local name `group`
holoviews/core/data/spatialpandas.py:738:9: PLR1704 Redefining argument with the local name `geom`
holoviews/core/options.py:1421:17: PLR1704 Redefining argument with the local name `cls`
holoviews/core/options.py:1603:45: PLR1704 Redefining argument with the local name `backends`
holoviews/core/pprint.py:311:13: PLR1704 Redefining argument with the local name `attrpath`
holoviews/ipython/archive.py:102:9: F811 Redefinition of unused `auto` from line 48
holoviews/ipython/magics.py:179:35: PLR1736 [*] List index lookup in `enumerate()` loop
holoviews/ipython/magics.py:282:13: PLR1704 Redefining argument with the local name `line`
holoviews/plotting/bokeh/element.py:1178:17: PLR1704 Redefining argument with the local name `key`
holoviews/plotting/bokeh/element.py:1849:13: PLR1704 Redefining argument with the local name `style`
holoviews/plotting/bokeh/links.py:127:21: PLR1704 Redefining argument with the local name `link`
holoviews/plotting/plot.py:555:17: PLR1704 Redefining argument with the local name `key`
holoviews/plotting/plot.py:913:18: PLR1704 Redefining argument with the local name `opts`
```


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 75b25b119..3131969f3 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.10
+    rev: v0.5.0
     hooks:
       - id: ruff
         files: holoviews/|scripts/
diff --git a/doc/releases.md b/doc/releases.md
index 96df9ba57..8c59ab64f 100644
--- a/doc/releases.md
+++ b/doc/releases.md
@@ -2,6 +2,38 @@
 
 ## Version 1.19
 
+### Version 1.19.1
+
+**July 4, 2024**
+
+This micro release includes a number of bug fixes and enhancements, as well as compatibility updates.
+Thanks to @maximlt and @hoxbro for their contributions.
+
+### Enhancements
+
+- Add color to bars for Plotly backend ([#6294](https://github.com/holoviz/holoviews/pull/6294))
+
+### Bug fixes
+
+- Ensure no auto index is created when a kdim is part of a multi index ([#6299](https://github.com/holoviz/holoviews/pull/6299))
+- Fix `hv.Bars` with only one value in it ([#6301](https://github.com/holoviz/holoviews/pull/6301))
+- Set `zIndex` of Popup to 1000 ([#6297](https://github.com/holoviz/holoviews/pull/6297))
+- Handle boolean input in categorically grouped `Bars` plots ([#6304](https://github.com/holoviz/holoviews/pull/6304))
+- Also check for `pd.NA` in `is_nan` ([#6290](https://github.com/holoviz/holoviews/pull/6290))
+
+### Compatibility
+
+- Compatibility with Bokeh 3.5 ([#6267](https://github.com/holoviz/holoviews/pull/6267))
+- Compatibility with Matplotlib 3.9 ([#6307](https://github.com/holoviz/holoviews/pull/6307))
+- pandas: Fix observed FutureWarning for categorial groupby ([#6306](https://github.com/holoviz/holoviews/pull/6306))
+- Lazy load `dask.dataframe` in `operations.datashader` ([#6309](https://github.com/holoviz/holoviews/pull/6309))
+
+### Maintenance
+
+- Update pre-commit and conda-build script ([#6287](https://github.com/holoviz/holoviews/pull/6287))
+- Update hatchling options to match other packages ([#6291](https://github.com/holoviz/holoviews/pull/6291))
+- Update `hv.show_versions` with more relevant packages ([#6293](https://github.com/holoviz/holoviews/pull/6293))
+
 ### Version 1.19.0
 
 **June 13, 2024**
diff --git a/holoviews/annotators.py b/holoviews/annotators.py
index b31842324..d74bd86b2 100644
--- a/holoviews/annotators.py
+++ b/holoviews/annotators.py
@@ -129,11 +129,11 @@ class annotate(param.ParameterizedFunction):
 
         layers = []
         annotator_type = None
-        for element in overlay:
+        for el in overlay:
             matches = []
             for eltype, atype in self._annotator_types.items():
-                if isinstance(element, eltype):
-                    matches.append((getmro(type(element)).index(eltype), atype))
+                if isinstance(el, eltype):
+                    matches.append((getmro(type(el)).index(eltype), atype))
             if matches:
                 if annotator_type is not None:
                     msg = ('An annotate call may only annotate a single element. '
@@ -142,12 +142,12 @@ class annotate(param.ParameterizedFunction):
                            'method to combine them into a single layout.')
                     raise ValueError(msg)
                 annotator_type = sorted(matches)[0][1]
-                self.annotator = annotator_type(element, **params)
+                self.annotator = annotator_type(el, **params)
                 tables = Overlay([t[0].object for t in self.annotator.editor], group='Annotator')
                 layout = (self.annotator.plot + tables)
                 layers.append(layout)
             else:
-                layers.append(element)
+                layers.append(el)
 
         if annotator_type is None:
             obj = overlay if isinstance(overlay, Overlay) else element
diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py
index 00ead6da6..516f13b1c 100644
--- a/holoviews/core/accessors.py
+++ b/holoviews/core/accessors.py
@@ -518,7 +518,7 @@ class Opts(metaclass=AccessorPipelineMeta):
         keywords = {}
         groups = Options._option_groups if group is None else [group]
         backend = backend if backend else Store.current_backend
-        for group in groups:
+        for group in groups:  # noqa: PLR1704
             optsobj = Store.lookup_options(backend, self._obj, group,
                                            defaults=defaults)
             keywords = dict(keywords, **optsobj.kwargs)
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/data/spatialpandas.py b/holoviews/core/data/spatialpandas.py
index 44fc86c96..6e62c5d7c 100644
--- a/holoviews/core/data/spatialpandas.py
+++ b/holoviews/core/data/spatialpandas.py
@@ -735,11 +735,11 @@ def to_spatialpandas(data, xdim, ydim, columns=None, geom='point'):
 
     array_type = None
     hole_arrays, geom_arrays = [], []
-    for geom in data:
-        geom = dict(geom)
-        if xdim not in geom or ydim not in geom:
+    for geom_data in data:
+        geom_data = dict(geom_data)
+        if xdim not in geom_data or ydim not in geom_data:
             raise ValueError('Could not find geometry dimensions')
-        xs, ys = geom.pop(xdim), geom.pop(ydim)
+        xs, ys = geom_data.pop(xdim), geom_data.pop(ydim)
         xscalar, yscalar = isscalar(xs), isscalar(ys)
         if xscalar and yscalar:
             xs, ys = np.array([xs]), np.array([ys])
@@ -754,7 +754,7 @@ def to_spatialpandas(data, xdim, ydim, columns=None, geom='point'):
 
         splits = np.where(np.isnan(geom_array[:, :2].astype('float')).sum(axis=1))[0]
         split_geoms = np.split(geom_array, splits+1) if len(splits) else [geom_array]
-        split_holes = geom.pop(Polygons._hole_key, None)
+        split_holes = geom_data.pop(Polygons._hole_key, None)
         if split_holes is not None:
             if len(split_holes) != len(split_geoms):
                 raise DataError('Polygons with holes containing multi-geometries '
@@ -776,7 +776,7 @@ def to_spatialpandas(data, xdim, ydim, columns=None, geom='point'):
             array_type = single_array
 
     converted = defaultdict(list)
-    for geom, arrays, holes in zip(data, geom_arrays, hole_arrays):
+    for geom_data, arrays, holes in zip(data, geom_arrays, hole_arrays):
         parts = []
         for i, g in enumerate(arrays):
             if i != (len(arrays)-1):
@@ -792,7 +792,7 @@ def to_spatialpandas(data, xdim, ydim, columns=None, geom='point'):
             if poly and holes is not None:
                 subparts += [np.array(h) for h in holes[i]]
 
-        for c, v in geom.items():
+        for c, v in geom_data.items():
             converted[c].append(v)
 
         if array_type is PointArray:
diff --git a/holoviews/core/options.py b/holoviews/core/options.py
index 35f7b4e79..b11b314f3 100644
--- a/holoviews/core/options.py
+++ b/holoviews/core/options.py
@@ -1418,9 +1418,9 @@ class Store:
         class_hierarchy = inspect.getmro(type(obj))
         hooks = []
         for _, type_hooks in cls._display_hooks.items():
-            for cls in class_hierarchy:
-                if cls in type_hooks:
-                    hooks.append(type_hooks[cls])
+            for subcls in class_hierarchy:
+                if subcls in type_hooks:
+                    hooks.append(type_hooks[subcls])
                     break
 
         data, metadata = {}, {}
@@ -1600,9 +1600,9 @@ class StoreOptions:
                 error_info[error_key+(backend,)] = error.allowed_keywords
                 backend_errors[error_key].add(backend)
 
-        for ((keyword, target, group_name), backends) in backend_errors.items():
+        for ((keyword, target, group_name), backend_error) in backend_errors.items():
             # If the keyword failed for the target across all loaded backends...
-            if set(backends) == set(loaded_backends):
+            if set(backend_error) == set(loaded_backends):
                 key = (keyword, target, group_name, Store.current_backend)
                 raise OptionError(keyword,
                                   group_name=group_name,
diff --git a/holoviews/core/pprint.py b/holoviews/core/pprint.py
index f3b070d3e..7faa42356 100644
--- a/holoviews/core/pprint.py
+++ b/holoviews/core/pprint.py
@@ -308,7 +308,7 @@ class PrettyPrinter(param.Parameterized):
         level, lines = cls_or_slf.node_info(node, attrpath, attrpaths, siblings, level, value_dims)
         attrpaths = ['.'.join(k) for k in node.keys()] if  hasattr(node, 'children') else []
         siblings = [node.get(child) for child in attrpaths]
-        for attrpath in attrpaths:
+        for attrpath in attrpaths:  # noqa: PLR1704
             lines += cls_or_slf.recurse(node.get(attrpath), attrpath, attrpaths=attrpaths,
                                  siblings=siblings, level=level+1, value_dims=value_dims)
         return lines
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index dadba0c08..24e95e271 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -1,6 +1,8 @@
 import builtins
 import datetime as dt
+import functools
 import hashlib
+import importlib
 import inspect
 import itertools
 import json
@@ -2031,7 +2033,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
 
@@ -2329,3 +2333,30 @@ def flatten(line):
             yield from flatten(element)
         else:
             yield element
+
+
+def lazy_isinstance(obj, class_or_tuple):
+    """ Lazy isinstance check
+
+    Will only import the module of the object if the module of the
+    obj matches the first value of an item in class_or_tuple.
+
+    lazy_isinstance(obj, 'dask.dataframe:DataFrame')
+
+    Will:
+        1) check if the first module is dask
+        2) If it dask, import dask.dataframe
+        3) Do an isinstance check for dask.dataframe.DataFrame
+    """
+    if isinstance(class_or_tuple, str):
+        class_or_tuple = (class_or_tuple,)
+
+    obj_mod_name = obj.__module__.split('.')[0]
+    for cls in class_or_tuple:
+        mod_name, _, attr_name = cls.partition(':')
+        if not obj_mod_name.startswith(mod_name.split(".")[0]):
+            continue
+        mod = importlib.import_module(mod_name)
+        if isinstance(obj, functools.reduce(getattr, attr_name.split('.'), mod)):
+            return True
+    return False
diff --git a/holoviews/ipython/archive.py b/holoviews/ipython/archive.py
index 504247f41..8155e4c0a 100644
--- a/holoviews/ipython/archive.py
+++ b/holoviews/ipython/archive.py
@@ -44,9 +44,6 @@ class NotebookArchive(FileArchive):
         Similar to FileArchive.filename_formatter except with support
         for the notebook name field as {notebook}.""")
 
-
-    auto = param.Boolean(False)
-
     # Used for debugging to view Exceptions raised from Javascript
     traceback = None
 
diff --git a/holoviews/ipython/magics.py b/holoviews/ipython/magics.py
index 801d45bbd..a79ecceb9 100644
--- a/holoviews/ipython/magics.py
+++ b/holoviews/ipython/magics.py
@@ -176,7 +176,7 @@ class OptsCompleter:
         for i, token in enumerate(tokens):
             key_checks =[]
             if i >= 0:  # Undotted key
-                key_checks.append(tokens[i])
+                key_checks.append(token)
             if i >= 1:  # Single dotted key
                 key_checks.append('.'.join([key_checks[-1], tokens[i-1]]))
             if i >= 2:  # Double dotted key
@@ -279,11 +279,11 @@ class OptsMagic(Magics):
         """
         if cell is None: return (line, cell)
         specs, code = [line], []
-        for line in cell.splitlines():
-            if line.strip().startswith('%%opts'):
-                specs.append(line.strip()[7:])
+        for cell_line in cell.splitlines():
+            if cell_line.strip().startswith('%%opts'):
+                specs.append(cell_line.strip()[7:])
             else:
-                code.append(line)
+                code.append(cell_line)
         return ' '.join(specs), '\n'.join(code)
 
 
diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py
index 66013cd98..b24d28c7f 100644
--- a/holoviews/operation/datashader.py
+++ b/holoviews/operation/datashader.py
@@ -2,7 +2,6 @@ import warnings
 from collections.abc import Callable, Iterable
 from functools import partial
 
-import dask.dataframe as dd
 import datashader as ds
 import datashader.reductions as rd
 import datashader.transfer_functions as tf
@@ -45,6 +44,7 @@ from ..core.util import (
     datetime_types,
     dt_to_int,
     get_param_values,
+    lazy_isinstance,
 )
 from ..element import (
     RGB,
@@ -303,22 +303,23 @@ class aggregate(LineAggregationOperation):
         if len(paths) > 1:
             if glyph == 'line':
                 path = paths[0][:1]
-                if isinstance(path, dd.DataFrame):
+                if lazy_isinstance(path, "dask.dataframe:DataFrame"):
                     path = path.compute()
                 empty = path.copy()
                 empty.iloc[0, :] = (np.nan,) * empty.shape[1]
                 paths = [elem for p in paths for elem in (p, empty)][:-1]
-            if all(isinstance(path, dd.DataFrame) for path in paths):
+            if all(lazy_isinstance(path,"dask.dataframe:DataFrame") for path in paths):
+                import dask.dataframe as dd
                 df = dd.concat(paths)
             else:
-                paths = [p.compute() if isinstance(p, dd.DataFrame) else p for p in paths]
+                paths = [p.compute() if lazy_isinstance(p, "dask.dataframe:DataFrame") else p for p in paths]
                 df = pd.concat(paths)
         else:
             df = paths[0] if paths else pd.DataFrame([], columns=[x.name, y.name])
         if category and df[category].dtype.name != 'category':
             df[category] = df[category].astype('category')
 
-        is_custom = isinstance(df, dd.DataFrame) or cuDFInterface.applies(df)
+        is_custom = lazy_isinstance(df, "dask.dataframe:DataFrame") or cuDFInterface.applies(df)
         if any((not is_custom and len(df[d.name]) and isinstance(df[d.name].values[0], cftime_types)) or
                df[d.name].dtype.kind in ["M", "u"] for d in (x, y)):
             df = df.copy()
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..791eca9bc 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -1175,11 +1175,11 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             if wheel_zoom:
                 wheel_zoom[0].zoom_on_axis = False
         elif isinstance(axis_obj, CategoricalAxis):
-            for key in list(axis_props):
-                if key.startswith('major_label'):
+            for axis_prop in list(axis_props):
+                if axis_prop.startswith('major_label'):
                     # set the group labels equal to major (actually minor)
-                    new_key = key.replace('major_label', 'group')
-                    axis_props[new_key] = axis_props[key]
+                    new_axis_prop = axis_prop.replace('major_label', 'group')
+                    axis_props[new_axis_prop] = axis_props[axis_prop]
 
             # major ticks are actually minor ticks in a categorical
             # so if user inputs minor ticks sizes, then use that;
@@ -1846,7 +1846,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
             new_style[k] = key
 
         # Process color/alpha styles and expand to fill/line style
-        for style, val in list(new_style.items()):
+        for style, val in new_style.copy().items():  # noqa: PLR1704
             for s in ('alpha', 'color'):
                 if prefix+s != style or style not in data or validate(s, val, True):
                     continue
@@ -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/bokeh/links.py b/holoviews/plotting/bokeh/links.py
index f494ca52a..5cf61d738 100644
--- a/holoviews/plotting/bokeh/links.py
+++ b/holoviews/plotting/bokeh/links.py
@@ -124,14 +124,14 @@ class LinkCallback:
                 if not plot._sources_match(link_src, source):
                     continue
                 links = []
-                for link in src_links:
+                for src_link in src_links:
                     # Skip if Link.target is an overlay but the plot isn't
                     # or if the target is an element but the plot isn't
-                    src = getattr(link, attr)
+                    src = getattr(src_link, attr)
                     src_el = src.last if isinstance(src, HoloMap) else src
                     if not plot._matching_plot_type(src_el):
                         continue
-                    links.append(link)
+                    links.append(src_link)
                 if links:
                     return (plot, links)
 
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/plotting/plot.py b/holoviews/plotting/plot.py
index 6ac6ef052..eb963afea 100644
--- a/holoviews/plotting/plot.py
+++ b/holoviews/plotting/plot.py
@@ -552,7 +552,7 @@ class DimensionedPlot(Plot):
         if unknown_keys:
             msg = "Popping unknown keys %r from fontsize dictionary.\nValid keys: %r"
             self.param.warning(msg %  (list(unknown_keys), self._fontsize_keys))
-            for key in unknown_keys: fontsize.pop(key, None)
+            for unknown_key in unknown_keys: fontsize.pop(unknown_key, None)
 
         defaults = self._get_fontsize_defaults()
         size = None
@@ -910,11 +910,11 @@ class DimensionedPlot(Plot):
         traversed = obj.traverse(lookup, specs)
         options = {}
         default_opts = defaultdict(lambda: defaultdict(list))
-        for key, opts in traversed:
-            defaults = opts.pop('defaults', {})
+        for key, topts in traversed:
+            defaults = topts.pop('defaults', {})
             if key not in options:
                 options[key] = {}
-            for opt, v in opts.items():
+            for opt, v in topts.items():
                 if opt not in options[key]:
                     options[key][opt] = []
                 options[key][opt].append(v)
@@ -922,8 +922,8 @@ class DimensionedPlot(Plot):
                 default_opts[key][opt].append(v)
 
         # Merge defaults into dictionary if not explicitly specified
-        for key, opts in default_opts.items():
-            for opt, v in opts.items():
+        for key, dopts in default_opts.items():
+            for opt, v in dopts.items():
                 if opt not in options[key]:
                     options[key][opt] = v
         return options if keyfn else options.get(None, {})
diff --git a/holoviews/plotting/plotly/chart.py b/holoviews/plotting/plotly/chart.py
index fcb223558..bcb7c82ee 100644
--- a/holoviews/plotting/plotly/chart.py
+++ b/holoviews/plotting/plotly/chart.py
@@ -198,8 +198,7 @@ class BarPlot(BarsMixin, ElementPlot):
     show_legend = param.Boolean(default=True, doc="""
         Whether to show legend for the plot.""")
 
-
-    style_opts = ['visible']
+    style_opts = ['visible', 'color']
 
     selection_display = PlotlyOverlaySelectionDisplay()
 
@@ -282,6 +281,12 @@ class BarPlot(BarsMixin, ElementPlot):
 
         return bars
 
+    def graph_options(self, element, ranges, style, **kwargs):
+        if 'color' in style:
+            style['marker_color'] = style.pop('color')
+        opts = super().graph_options(element, ranges, style, **kwargs)
+        return opts
+
     def init_layout(self, key, element, ranges, **kwargs):
         layout = super().init_layout(key, element, ranges)
         stack_dim = None
diff --git a/pyproject.toml b/pyproject.toml
index 4ff028092..a95d6e4d5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -119,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]


Test patch:
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'])
diff --git a/holoviews/tests/plotting/plotly/test_barplot.py b/holoviews/tests/plotting/plotly/test_barplot.py
index bf0a7ea71..3d0fb3443 100644
--- a/holoviews/tests/plotting/plotly/test_barplot.py
+++ b/holoviews/tests/plotting/plotly/test_barplot.py
@@ -155,3 +155,10 @@ class TestBarsPlot(TestPlotlyPlot):
         plot = self._get_plot_state(bars)
         np.testing.assert_equal(set(plot['data'][0]['x']), set(pets))
         np.testing.assert_equal(plot['data'][0]['y'], np.array([8, 7, 6, 7]))
+
+    def test_bar_color(self):
+        data = pd.DataFrame({"A": range(5)})
+        bars = Bars(data).opts(color="gold")
+        fig = self._get_plot_state(bars)
+        data = fig["data"][0]
+        assert data["marker"]["color"] == "gold"