Repository: holoviz/holoviews
Pull request #6294 (2024-06-24 07:29:32)

Add color to bars for Plotly
Fixes https://github.com/holoviz/holoviews/issues/5658

![image](https://github.com/holoviz/holoviews/assets/19758978/fe497b86-22a8-4bef-8947-927b898a5973)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6294?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.52%. 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 [(`4c6e270`)](https://app.codecov.io/gh/holoviz/holoviews/commit/4c6e27084f84fb7b7c59d108b0429f00e615bdab?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).
> Report is 1 commits behind head on main.

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


```diff
@@            Coverage Diff             @@
##             main    #6294      +/-   ##
==========================================
- Coverage   88.55%   88.52%   -0.04%     
==========================================
  Files         323      323              
  Lines       68000    68006       +6     
==========================================
- Hits        60218    60199      -19     
- Misses       7782     7807      +25     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6294?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/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/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/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 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]
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_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"