diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index 36dec7c601..e608ed0bf2 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -36,6 +36,10 @@ class HoloViews(PaneBase): center = param.Boolean(default=False, doc=""" Whether to center the plot.""") + linked_axes = param.Boolean(default=True, doc=""" + Whether to use link the axes of bokeh plots inside this pane + across a panel layout.""") + widget_location = param.ObjectSelector(default='right_top', objects=[ 'left', 'bottom', 'right', 'top', 'top_left', 'top_right', 'bottom_left', 'bottom_right', 'left_top', 'left_bottom', @@ -409,4 +413,47 @@ def find_links(root_view, root_model): return callbacks +def link_axes(root_view, root_model): + """ + Pre-processing hook to allow linking axes across HoloViews bokeh + plots. + """ + panes = root_view.select(HoloViews) + + if not panes: + return + + from holoviews.core.options import Store + from holoviews.plotting.bokeh.element import ElementPlot + + ref = root_model.ref['id'] + range_map = defaultdict(list) + for pane in panes: + if ref not in pane._plots: + continue + plot = pane._plots[ref][0] + if not pane.linked_axes or plot.renderer.backend != 'bokeh': + continue + for p in plot.traverse(specs=[ElementPlot]): + axiswise = Store.lookup_options('bokeh', p.current_frame, 'norm').kwargs.get('axiswise') + if not p.shared_axes or axiswise: + continue + + fig = p.state + if fig.x_range.tags: + range_map[fig.x_range.tags[0]].append((fig, p, fig.x_range)) + if fig.y_range.tags: + range_map[fig.y_range.tags[0]].append((fig, p, fig.y_range)) + + for tag, axes in range_map.items(): + fig, p, axis = axes[0] + for fig, p, _ in axes[1:]: + if tag in fig.x_range.tags and not axis is fig.x_range: + fig.x_range = axis + p.handles['x_range'] = axis + if tag in fig.y_range.tags and not axis is fig.y_range: + fig.y_range = axis + p.handles['y_range'] = axis + +Viewable._preprocessing_hooks.append(link_axes) Viewable._preprocessing_hooks.append(find_links) diff --git a/panel/tests/pane/test_holoviews.py b/panel/tests/pane/test_holoviews.py index 1ec710f35c..3eceade417 100644 --- a/panel/tests/pane/test_holoviews.py +++ b/panel/tests/pane/test_holoviews.py @@ -257,7 +257,7 @@ def test_holoviews_layouts(document, comm): hv_pane = HoloViews(hmap, backend='bokeh') layout = hv_pane.layout model = layout.get_root(document, comm) - + for center in (True, False): for loc in HoloViews.param.widget_location.objects: hv_pane.set_param(center=center, widget_location=loc) @@ -386,6 +386,86 @@ def test_holoviews_widgets_explicit_widget_instance_override(): assert widgets[0] is widget +@hv_available +def test_holoviews_linked_axes(document, comm): + c1 = hv.Curve([1, 2, 3]) + c2 = hv.Curve([1, 2, 3]) + + layout = Row(HoloViews(c1, backend='bokeh'), HoloViews(c2, backend='bokeh')) + + row_model = layout.get_root(document, comm=comm) + + print(row_model.children) + + p1, p2 = row_model.select({'type': Figure}) + + assert p1.x_range is p2.x_range + assert p1.y_range is p2.y_range + + +@hv_available +def test_holoviews_linked_x_axis(document, comm): + c1 = hv.Curve([1, 2, 3]) + c2 = hv.Curve([1, 2, 3], vdims='y2') + + layout = Row(HoloViews(c1, backend='bokeh'), HoloViews(c2, backend='bokeh')) + + row_model = layout.get_root(document, comm=comm) + + p1, p2 = row_model.select({'type': Figure}) + + assert p1.x_range is p2.x_range + assert p1.y_range is not p2.y_range + + +@hv_available +def test_holoviews_axiswise_not_linked_axes(document, comm): + c1 = hv.Curve([1, 2, 3]) + c2 = hv.Curve([1, 2, 3]).opts(axiswise=True, backend='bokeh') + + layout = Row(HoloViews(c1, backend='bokeh'), HoloViews(c2, backend='bokeh')) + + row_model = layout.get_root(document, comm=comm) + + p1, p2 = row_model.select({'type': Figure}) + + assert p1.x_range is not p2.x_range + assert p1.y_range is not p2.y_range + + +@hv_available +def test_holoviews_shared_axes_opt_not_linked_axes(document, comm): + c1 = hv.Curve([1, 2, 3]) + c2 = hv.Curve([1, 2, 3]).opts(shared_axes=False, backend='bokeh') + + layout = Row(HoloViews(c1, backend='bokeh'), HoloViews(c2, backend='bokeh')) + + row_model = layout.get_root(document, comm=comm) + + p1, p2 = row_model.select({'type': Figure}) + + assert p1.x_range is not p2.x_range + assert p1.y_range is not p2.y_range + + +@hv_available +def test_holoviews_not_linked_axes(document, comm): + c1 = hv.Curve([1, 2, 3]) + c2 = hv.Curve([1, 2, 3]) + + layout = Row( + HoloViews(c1, backend='bokeh'), + HoloViews(c2, backend='bokeh', linked_axes=False) + ) + + row_model = layout.get_root(document, comm=comm) + + p1, p2 = row_model.select({'type': Figure}) + + assert p1.x_range is not p2.x_range + assert p1.y_range is not p2.y_range + + @hv_available def test_holoviews_link_across_panes(document, comm): from bokeh.models.tools import RangeTool