From d4bbaef890f7115ff6bfc35029368354d2a15b7d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Aug 2019 17:18:17 +0200 Subject: [PATCH 1/2] Add preprocessing hook to link axes --- panel/pane/holoviews.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index 36dec7c601..d3eab12a34 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=False, 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', @@ -408,5 +412,29 @@ def find_links(root_view, root_model): root_view._found_links.update(new_found) return callbacks - +def link_axes(root_view, root_model): + ref = root_model.ref['id'] + range_map = defaultdict(list) + for pane in root_view.select(pn.pane.HoloViews): + if not pane.linked_axes: + continue + for p in pane._plots[ref][0].traverse(specs=[ElementPlot]): + 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) From 06a9f188a3a13741657ac9557b258c416a6628ca Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Aug 2019 16:53:31 +0200 Subject: [PATCH 2/2] Improvements and fixes for axis linking --- panel/pane/holoviews.py | 29 +++++++++-- panel/tests/pane/test_holoviews.py | 82 +++++++++++++++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py index d3eab12a34..e608ed0bf2 100644 --- a/panel/pane/holoviews.py +++ b/panel/pane/holoviews.py @@ -36,7 +36,7 @@ class HoloViews(PaneBase): center = param.Boolean(default=False, doc=""" Whether to center the plot.""") - linked_axes = param.Boolean(default=False, doc=""" + linked_axes = param.Boolean(default=True, doc=""" Whether to use link the axes of bokeh plots inside this pane across a panel layout.""") @@ -412,20 +412,39 @@ def find_links(root_view, root_model): root_view._found_links.update(new_found) 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 root_view.select(pn.pane.HoloViews): - if not pane.linked_axes: + 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 pane._plots[ref][0].traverse(specs=[ElementPlot]): + 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:]: 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