diff --git a/examples/user_guide/Plotting_with_Bokeh.ipynb b/examples/user_guide/Plotting_with_Bokeh.ipynb index 2524e1671c..1e7fa587b6 100644 --- a/examples/user_guide/Plotting_with_Bokeh.ipynb +++ b/examples/user_guide/Plotting_with_Bokeh.ipynb @@ -489,7 +489,53 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The other ``muted_`` options can be used to define other aspects of the Histogram style when it is unselected." + "The other ``muted_`` options can be used to define other aspects of the Histogram style when it is unselected.\n", + "\n", + "If you have multiple plots in a ``Layout`` with the same legend label, muting one of them will automatically mute all of them. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "overlay1 = hv.Curve([0, 0], label=\"A\") * hv.Curve([1, 1], label=\"B\")\n", + "overlay2 = hv.Curve([2, 2], label=\"A\") * hv.Curve([3, 3], label=\"B\")\n", + "layout = overlay1 + overlay2\n", + "layout " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to turn off this behavior, use ``.opts(sync_legends=False)``" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "layout.opts(sync_legends=False)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want to control the number of legend shown in the ``Layout`` or the position of them ``show_legends`` and ``legend_position`` can be used." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "layout.opts(sync_legends=True, show_legends=0, legend_position=\"top_left\")" ] }, { @@ -914,5 +960,5 @@ } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 } diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index e6267edb6d..9c936b901d 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -31,7 +31,7 @@ from .links import LinkCallback from .util import ( bokeh3, filter_toolboxes, make_axis, sync_legends, update_shared_sources, empty_plot, - decode_bytes, theme_attr_json, cds_column_replace, get_default, merge_tools, + decode_bytes, theme_attr_json, cds_column_replace, get_default, merge_tools, select_legends ) if bokeh3: @@ -690,6 +690,21 @@ class LayoutPlot(CompositePlot, GenericLayoutPlot): sync_legends = param.Boolean(default=True, doc=""" Whether to sync the legend when muted/unmuted based on the name""") + show_legends = param.ClassSelector(default=None, class_=(list, int, bool), doc=""" + Whether to show the legend for a particular subplot by index. If True all legends + will be shown. If False no legends will be shown.""") + + legend_position = param.ObjectSelector(objects=["top_right", + "top_left", + "bottom_left", + "bottom_right", + 'right', 'left', + 'top', 'bottom'], + default="top_right", + doc=""" + Allows selecting between a number of predefined legend position + options. Will only be applied if show_legend is not None.""") + tabs = param.Boolean(default=False, doc=""" Whether to display overlaid plots in separate panes""") @@ -700,6 +715,11 @@ def __init__(self, layout, keys=None, **params): self.traverse(lambda x: attach_streams(self, x.hmap, 2), [GenericElementPlot]) + @param.depends('show_legends', 'legend_position', watch=True, on_init=True) + def _update_show_legend(self): + if self.show_legends is not None: + select_legends(self.layout, self.show_legends, self.legend_position) + def _init_layout(self, layout): # Situate all the Layouts in the grid and compute the gridspec # indices for all the axes required by each LayoutPlot. diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 55082eba53..ca4df5c41d 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -33,7 +33,7 @@ from ...core.layout import Layout from ...core.ndmapping import NdMapping -from ...core.overlay import Overlay +from ...core.overlay import Overlay, NdOverlay from ...core.util import ( arraylike_types, callable_name, cftime_types, cftime_to_timestamp, isnumeric, pd, unique_array @@ -468,20 +468,25 @@ def select_legends(holoviews_layout, figure_index=None, legend_position="top_rig ---------- holoviews_layout : Holoviews Layout Holoviews Layout with legends. - figure_index : list[int] | int | None + figure_index : list[int] | bool | int | None Index of the figures which legends to show. If None is chosen, only the first figures legend is shown + If True is chosen, all legends are shown. legend_position : str Position of the legend(s). """ if figure_index is None: figure_index = [0] + elif isinstance(figure_index, bool): + figure_index = range(len(holoviews_layout)) if figure_index else [] elif isinstance(figure_index, int): figure_index = [figure_index] if not isinstance(holoviews_layout, Layout): holoviews_layout = [holoviews_layout] for i, plot in enumerate(holoviews_layout): + if not isinstance(plot, (NdOverlay, Overlay)): + continue if i in figure_index: plot.opts(show_legend=True, legend_position=legend_position) else: diff --git a/holoviews/tests/plotting/bokeh/test_utils.py b/holoviews/tests/plotting/bokeh/test_utils.py index 2c254ec9f0..b585b6f32d 100644 --- a/holoviews/tests/plotting/bokeh/test_utils.py +++ b/holoviews/tests/plotting/bokeh/test_utils.py @@ -1,6 +1,9 @@ +import pytest + +import holoviews as hv from holoviews.core import Store from holoviews.element.comparison import ComparisonTestCase -from holoviews.plotting.bokeh.util import filter_batched_data, glyph_order +from holoviews.plotting.bokeh.util import filter_batched_data, glyph_order, select_legends from holoviews.plotting.bokeh.styles import expand_batched_style bokeh_renderer = Store.renderers['bokeh'] @@ -74,3 +77,28 @@ def test_glyph_order(self): order = glyph_order(['scatter_1', 'patch_1', 'rect_1'], ['scatter', 'patch']) self.assertEqual(order, ['scatter_1', 'patch_1', 'rect_1']) + + +@pytest.mark.parametrize( + "figure_index,expected", + [ + (0, [True, False]), + (1, [False, True]), + ([0], [True, False]), + ([1], [False, True]), + ([0, 1], [True, True]), + (True, [True, True]), + (False, [False, False]), + (None, [True, False]), + ], + ids=["int0", "int1", "list0", "list1", "list01", "True", "False", "None"], +) +def test_select_legends_figure_index(figure_index, expected): + overlays = [ + hv.Curve([0, 0]) * hv.Curve([1, 1]), + hv.Curve([2, 2]) * hv.Curve([3, 3]), + ] + layout = hv.Layout(overlays) + select_legends(layout, figure_index) + output = [ol.opts["show_legend"] for ol in overlays] + assert expected == output