Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Legend support for Layout and add documentation #5852

Merged
merged 5 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions examples/user_guide/Plotting_with_Bokeh.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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\")"
]
},
{
Expand Down Expand Up @@ -914,5 +960,5 @@
}
},
"nbformat": 4,
"nbformat_minor": 1
"nbformat_minor": 4
}
22 changes: 21 additions & 1 deletion holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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""")

Expand All @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
30 changes: 29 additions & 1 deletion holoviews/tests/plotting/bokeh/test_utils.py
Original file line number Diff line number Diff line change
@@ -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']
Expand Down Expand Up @@ -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