Repository: holoviz/holoviews
Pull request #6544 (2025-03-17 15:26:57)

fix: add legend to distribution mpl backend
Fixes https://github.com/holoviz/holoviews/issues/5686

Having a legend aligns the Distribution with the Matplotlib backend and how the Bokeh backend works. 

``` python
import holoviews as hv
import numpy as np
import pandas as pd

hv.extension("matplotlib")

# example data
normal1 = np.random.randn(1000)
normal2 = np.random.randn(1000)
binary = np.random.randint(0, 2, 1000)
df = pd.DataFrame({"normal1": normal1, "normal2": normal2, "binary": binary})

ds = hv.Dataset(df, kdims=["binary"], vdims=["normal1", "normal2"])
ds.to(hv.Distribution, "normal1", "normal2").overlay("binary")
```


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4ae094661..cd45cce8c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,12 +17,12 @@ repos:
       - id: check-json
       - id: detect-private-key
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.10.0
+    rev: v0.11.0
     hooks:
       - id: ruff
         exclude: \.ipynb$
   - repo: https://github.com/hoxbro/clean_notebook
-    rev: v0.1.15
+    rev: v0.1.17
     hooks:
       - id: clean-notebook
         args: [--strip-trailing-newlines]
diff --git a/holoviews/element/stats.py b/holoviews/element/stats.py
index ac24b118d..83be95996 100644
--- a/holoviews/element/stats.py
+++ b/holoviews/element/stats.py
@@ -30,11 +30,13 @@ class StatisticsElement(Dataset, Element2D):
         params.update(dict(kdims=kdims, vdims=[], _validate_vdims=False))
         super().__init__(data, **params)
         if not vdims:
-            self.vdims = [Dimension('Density')]
+            with param.edit_constant(self):
+                self.vdims = [Dimension('Density')]
         elif len(vdims) > 1:
             raise ValueError(f"{type(self).__name__} expects at most one vdim.")
         else:
-            self.vdims = process_dimensions(None, vdims)['vdims']
+            with param.edit_constant(self):
+                self.vdims = process_dimensions(None, vdims)['vdims']
 
     @property
     def dataset(self):
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index 53388315e..b7376ac05 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -919,7 +919,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
 
         axis_specs = {'x': {}, 'y': {}}
         axis_specs['x']['x'] = (*self._axis_props(plots, subplots, element, ranges, pos=0), self.xaxis, {})
-        if self.multi_y:
+        if self.multi_y and subplots:
             if not BOKEH_GE_3_2_0:
                 self.param.warning('Independent axis zooming for multi_y=True only supported for Bokeh >=3.2')
             yaxes, extra_axis_specs = self._create_extra_axes(plots, subplots, element, ranges)
diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py
index 778b911cb..ae490639c 100644
--- a/holoviews/plotting/mpl/__init__.py
+++ b/holoviews/plotting/mpl/__init__.py
@@ -312,4 +312,5 @@ options.Sankey = Options('style', edge_color='grey', node_edgecolors='black',
 # Statistics
 options.Distribution = Options('style', facecolor=Cycle(), edgecolor='black',
                                alpha=0.5)
+options.Distribution = Options('plot', show_legend=True)
 options.Violin = Options('style', facecolors=Cycle(), showextrema=False, alpha=0.7)
diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py
index 915ee6cff..7562b1cc3 100644
--- a/holoviews/plotting/mpl/chart.py
+++ b/holoviews/plotting/mpl/chart.py
@@ -369,6 +369,8 @@ class HistogramPlot(ColorbarPlot):
         xlim = hist.range(0)
         ylim = hist.range(1)
         is_datetime = isdatetime(edges)
+        if hasattr(edges, "compute"):
+            edges = edges.compute()
         if is_datetime:
             edges = np.array([dt64_to_dt(e) if isinstance(e, np.datetime64) else e for e in edges])
             edges = date2num(edges)
diff --git a/pixi.toml b/pixi.toml
index 5479d1d5a..60bf033ce 100644
--- a/pixi.toml
+++ b/pixi.toml
@@ -171,7 +171,7 @@ nbval = "*"
 channels = ["microsoft"]
 
 [feature.test-ui.dependencies]
-playwright = { version = "*", channel = "microsoft" }
+playwright = { version = "!=1.51.0", channel = "microsoft" } # https://github.com/microsoft/playwright-python/issues/2791
 pytest-playwright = { version = "*", channel = "microsoft" }
 
 [feature.test-ui.tasks]


Test patch:
diff --git a/holoviews/tests/operation/test_operation.py b/holoviews/tests/operation/test_operation.py
index 01fd131f6..ff20fb4ec 100644
--- a/holoviews/tests/operation/test_operation.py
+++ b/holoviews/tests/operation/test_operation.py
@@ -593,6 +593,25 @@ class OperationTests(ComparisonTestCase):
         hist = Histogram(([1.,  4., 7.5], [0, 3, 6, 9]), vdims=['y'])
         self.assertEqual(op_hist, hist)
 
+    @pytest.mark.usefixtures("mpl_backend")
+    def test_histogram_dask_array_mpl(self):
+        # Regression test for https://github.com/holoviz/holoviews/issues/5111
+        dd = pytest.importorskip("dask.dataframe")
+
+        data = {
+            "carrier": ["A", "A", "A", "A", "B", "B", "B", "B", "B"],
+            "depdelay": [127.0, 3.0, -3.0, 19.0, 264.0, -6.0, 1.0, 2.0, 83.0],
+        }
+        flights = dd.from_pandas(pd.DataFrame(data), npartitions=2)
+
+        by = "carrier"
+        ds = Dataset(flights, by)
+        ds_grouped = ds.groupby(by)
+        hists = histogram(ds_grouped, dimension="depdelay")
+
+        # Should not error
+        renderer("matplotlib").get_plot(hists)
+
     def test_interpolate_curve_pre(self):
         interpolated = interpolate_curve(Curve([0, 0.5, 1]), interpolation='steps-pre')
         curve = Curve([(0, 0), (0, 0.5), (1, 0.5), (1, 1), (2, 1)])
diff --git a/holoviews/tests/plotting/bokeh/test_multiaxis.py b/holoviews/tests/plotting/bokeh/test_multiaxis.py
index 540351b31..0024d12e6 100644
--- a/holoviews/tests/plotting/bokeh/test_multiaxis.py
+++ b/holoviews/tests/plotting/bokeh/test_multiaxis.py
@@ -306,3 +306,10 @@ class TestCurveTwinAxes(LoggingComparisonTestCase, TestBokehPlot):
         assert plot.state.yaxis[0].axis_label_text_font_size == '15pt'
         assert plot.state.yaxis[1].axis_label == 'A'
         assert plot.state.yaxis[1].axis_label_text_font_size == '13pt'
+
+    def test_multi_y_on_curve(self):
+        # Test for https://github.com/holoviz/holoviews/issues/6322
+        overlay = Curve(range(10), vdims='A').opts(multi_y=True)
+
+        # Should not error
+        bokeh_renderer.get_plot(overlay)
diff --git a/holoviews/tests/plotting/matplotlib/test_distribution.py b/holoviews/tests/plotting/matplotlib/test_distribution.py
new file mode 100644
index 000000000..c3b6fd9d3
--- /dev/null
+++ b/holoviews/tests/plotting/matplotlib/test_distribution.py
@@ -0,0 +1,14 @@
+import pytest
+
+import holoviews as hv
+
+
+@pytest.mark.usefixtures("mpl_backend")
+def test_distribution_legend(rng):
+    normal = rng.normal(1000)
+    binary = rng.integers(0, 2, 1000)
+    df = {"normal": normal, "binary": binary}
+
+    ds = hv.Dataset(df, kdims=["binary"], vdims=["normal"])
+    plot = ds.to(hv.Distribution, "normal").overlay("binary")
+    assert "legend" in hv.renderer("matplotlib").get_plot(plot).handles