From 380feed8ea268f3b5ed90898c5f9144120be0168 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 4 Jun 2024 15:14:37 +0200 Subject: [PATCH 01/13] Ensure Range streams and RangeToolLink respect subcoordinate axis range --- holoviews/plotting/bokeh/callbacks.py | 5 ++++- holoviews/plotting/bokeh/element.py | 1 + holoviews/plotting/bokeh/links.py | 21 +++++++++++++-------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 82698dbf73..ba8bb480ae 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -858,7 +858,10 @@ def _process_msg(self, msg): x_range = self.plot.handles['x_range'] msg['x0'], msg['x1'] = x_range.start, x_range.end if self.plot.state.y_range is not self.plot.handles['y_range']: - y_range = self.plot.handles['y_range'] + if 'subcoordinate_y_range' in self.plot.handles: + y_range = self.plot.handles['subcoordinate_y_range'] + else: + y_range = self.plot.handles['y_range'] msg['y0'], msg['y1'] = y_range.start, y_range.end data = {} if 'x0' in msg and 'x1' in msg: diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e46be71504..3b942b4581 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -2051,6 +2051,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): self.handles['x_range'], self.handles['y_range'] = plot_ranges if self._subcoord_overlaid: if style_element.label in plot.extra_y_ranges: + self.handles['subcoordinate_y_range'] = plot.y_range self.handles['y_range'] = plot.extra_y_ranges.pop(style_element.label) if self.apply_hard_bounds: diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 9aaea35fc5..aaac154b78 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -141,25 +141,30 @@ def __init__(self, root_model, link, source_plot, target_plot): if axis not in link.axes: continue - axes[f'{axis}_range'] = target_plot.handles[f'{axis}_range'] + range_name = f'{axis}_range' + if f'subcoordinate_{axis}_range' in target_plot.handles: + target_range_name = f'subcoordinate_{range_name}' + else: + target_range_name = range_name + axes[range_name] = target_plot.handles[target_range_name] interval = getattr(link, f'intervals{axis}', None) if interval is not None and bokeh34: min, max = interval if min is not None: - axes[f'{axis}_range'].min_interval = min + axes[range_name].min_interval = min if max is not None: - axes[f'{axis}_range'].max_interval = max - self._set_range_for_interval(axes[f'{axis}_range'], max) + axes[range_name].max_interval = max + self._set_range_for_interval(axes[range_name], max) bounds = getattr(link, f'bounds{axis}', None) if bounds is not None: start, end = bounds if start is not None: - axes[f'{axis}_range'].start = start - axes[f'{axis}_range'].reset_start = start + axes[range_name].start = start + axes[range_name].reset_start = start if end is not None: - axes[f'{axis}_range'].end = end - axes[f'{axis}_range'].reset_end = end + axes[range_name].end = end + axes[range_name].reset_end = end tool = RangeTool(**axes) source_plot.state.add_tools(tool) From ac68c01b5bb22714a516bef1df290f301b021ae6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 5 Jun 2024 19:42:36 +0200 Subject: [PATCH 02/13] Do not redraw subcoordinate axis labels --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 3b942b4581..6109dd72b7 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1073,7 +1073,7 @@ def _axis_properties(self, axis, key, plot, dimension=None, axis_props['ticker'] = FixedTicker(ticks=ticks) if labels is not None: axis_props['major_label_overrides'] = dict(zip(ticks, labels)) - elif self._subcoord_overlaid and axis == 'y': + elif self._subcoord_overlaid and axis == 'y' and not self.drawn: ticks, labels = [], [] idx = 0 for el, sp in zip(self.current_frame, self.subplots.values()): From cc2e84b90755a7e37746d1cf7266a4285f453254 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 15:53:22 +0200 Subject: [PATCH 03/13] Ensure stream callbacks on (Nd)Overlay are attached only to OverlayPlot --- holoviews/plotting/bokeh/callbacks.py | 5 +-- holoviews/plotting/bokeh/element.py | 6 ++- holoviews/plotting/plot.py | 24 +++++++++--- .../tests/plotting/bokeh/test_callbacks.py | 39 +++++++++++++++++++ holoviews/tests/ui/bokeh/test_callback.py | 30 ++++++++++++++ 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index ba8bb480ae..82698dbf73 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -858,10 +858,7 @@ def _process_msg(self, msg): x_range = self.plot.handles['x_range'] msg['x0'], msg['x1'] = x_range.start, x_range.end if self.plot.state.y_range is not self.plot.handles['y_range']: - if 'subcoordinate_y_range' in self.plot.handles: - y_range = self.plot.handles['subcoordinate_y_range'] - else: - y_range = self.plot.handles['y_range'] + y_range = self.plot.handles['y_range'] msg['y0'], msg['y1'] = y_range.start, y_range.end data = {} if 'x0' in msg and 'x1' in msg: diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index eef1b24bfd..01c6231158 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -291,7 +291,10 @@ def __init__(self, element, plot=None, **params): super().__init__(element, **params) self.handles = {} if plot is None else self.handles['plot'] self.static = len(self.hmap) == 1 and len(self.keys) == len(self.hmap) - self.callbacks, self.source_streams = self._construct_callbacks() + if isinstance(self, GenericOverlayPlot): + self.callbacks, self.source_streams = [], [] + else: + self.callbacks, self.source_streams = self._construct_callbacks() self.static_source = False self.streaming = [s for s in self.streams if isinstance(s, Buffer)] self.geographic = bool(self.hmap.last.traverse(lambda x: x, Tiles)) @@ -2840,6 +2843,7 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot): def __init__(self, overlay, **kwargs): self._multi_y_propagation = self.lookup_options(overlay, 'plot').options.get('multi_y', False) super().__init__(overlay, **kwargs) + self.callbacks, self.source_streams = self._construct_callbacks() self._multi_y_propagation = False @property diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index cc9bdbc489..cf6f598e28 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -979,10 +979,18 @@ def _construct_callbacks(self): registry = list(Stream.registry.items()) callbacks = Stream._callbacks[self.backend] for source in self.link_sources: - streams = [ - s for src, streams in registry for s in streams - if src is source or (src._plot_id is not None and - src._plot_id == source._plot_id)] + streams = [] + for src, src_streams in registry: + # Skip if source identities do not match + if (src is not source and (src._plot_id is None or src._plot_id != source._plot_id)): + continue + for stream in src_streams: + # Skip if Stream.source is an overlay but the plot isn't + if (isinstance(stream.source, DynamicMap) and + isinstance(stream.source.last, CompositeOverlay) and + not isinstance(self, GenericOverlayPlot)): + continue + streams.append(stream) cb_classes |= {(callbacks[type(stream)], stream) for stream in streams if type(stream) in callbacks and stream.linked and stream.source is not None} @@ -1007,7 +1015,13 @@ def link_sources(self): zorders = [self.zorder] if isinstance(self, GenericOverlayPlot) and not self.batched: - sources = [self.hmap.last] + if self.overlaid: + sources = [self.hmap.last] + else: + sources = [ + o for i, inputs in self.stream_sources.items() + for o in inputs + ] elif not self.static or isinstance(self.hmap, DynamicMap): sources = [o for i, inputs in self.stream_sources.items() for o in inputs if i in zorders] diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py index 1d6e523c8d..193448ffc2 100644 --- a/holoviews/tests/plotting/bokeh/test_callbacks.py +++ b/holoviews/tests/plotting/bokeh/test_callbacks.py @@ -487,3 +487,42 @@ def test_rangexy_multi_yaxes(): # Ensure both callbacks are attached assert p1.callbacks[0].plot is p1 assert p2.callbacks[0].plot is p2 + + +@pytest.mark.usefixtures('bokeh_backend') +def test_rangexy_subcoordinate_y(): + c1 = Curve(np.arange(100).cumsum(), vdims='y', label='A').opts(subcoordinate_y=True) + c2 = Curve(-np.arange(100).cumsum(), vdims='y2', label='B').opts(subcoordinate_y=True) + + overlay = (c1 * c2) + RangeXY(source=overlay) + + plot = bokeh_server_renderer.get_plot(overlay) + + p1, p2 = plot.subplots.values() + + assert not (p1.callbacks or p2.callbacks) + assert len(plot.callbacks) == 1 + callback = plot.callbacks[0] + assert callback._process_msg({}) == {} + + +@pytest.mark.usefixtures('bokeh_backend') +def test_rangexy_subcoordinate_y_dynamic(): + + def cb(x_range, y_range): + return ( + Curve(np.arange(100).cumsum(), vdims='y', label='A').opts(subcoordinate_y=True) * + Curve(-np.arange(100).cumsum(), vdims='y2', label='B').opts(subcoordinate_y=True) + ) + + stream = RangeXY() + dmap = DynamicMap(cb, streams=[stream]) + plot = bokeh_server_renderer.get_plot(dmap) + + p1, p2 = plot.subplots.values() + + assert not (p1.callbacks or p2.callbacks) + assert len(plot.callbacks) == 1 + callback = plot.callbacks[0] + assert callback._process_msg({}) == {} diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 93eb11de3a..395dec132f 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -376,3 +376,33 @@ def popup_form(index): locator = page.locator("#lasso") expect(locator).to_have_count(1) expect(locator).not_to_have_text("lasso\n0") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_subcoordinate_y_range(serve_hv, points): + def cb(x_range, y_range): + return ( + Curve(np.arange(100).cumsum(), vdims='y', label='A').opts(subcoordinate_y=True) * + Curve(-np.arange(100).cumsum(), vdims='y2', label='B').opts(subcoordinate_y=True) + ) + + stream = RangeXY() + dmap = DynamicMap(cb, streams=[stream]).opts(active_tools=['box_zoom']) + + page = serve_hv(dmap) + + hv_plot = page.locator('.bk-events') + + expect(hv_plot).to_have_count(1) + + bbox = hv_plot.bounding_box() + hv_plot.click() + + page.mouse.move(bbox['x']+60, bbox['y']+60) + page.mouse.down() + page.mouse.move(bbox['x']+190, bbox['y']+190, steps=5) + page.mouse.up() + + expected_xrange = (7.008849557522124, 63.95575221238938) + expected_yrange = (0.030612244897959183, 1.0918367346938775) + wait_until(lambda: stream.x_range == expected_xrange and stream.y_range == expected_yrange, page) From c283766fb93010e8361572d2f65bddb47066747f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 16:05:47 +0200 Subject: [PATCH 04/13] Tweaks in callback creation skip condition --- holoviews/plotting/plot.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index cf6f598e28..ab57d3dd4c 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -986,9 +986,12 @@ def _construct_callbacks(self): continue for stream in src_streams: # Skip if Stream.source is an overlay but the plot isn't - if (isinstance(stream.source, DynamicMap) and - isinstance(stream.source.last, CompositeOverlay) and - not isinstance(self, GenericOverlayPlot)): + # or if the source is an element but the plot isn't + src_el = stream.source.last if isinstance(stream.source, HoloMap) else stream.source + if ((isinstance(src_el, CompositeOverlay) and + not isinstance(self, GenericOverlayPlot)) or + (isinstance(src_el, Element) and + isinstance(self, GenericOverlayPlot))): continue streams.append(stream) cb_classes |= {(callbacks[type(stream)], stream) for stream in streams @@ -1002,6 +1005,7 @@ def _construct_callbacks(self): if cb_stream not in source_streams: source_streams.append(cb_stream) cbs.append(cb(self, cb_streams, source)) + print(cbs[-1], type(self)) return cbs, source_streams @property From 8127961aa7afe5f32848e0151d20f725c14b9f51 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 16:42:56 +0200 Subject: [PATCH 05/13] Fix review comments --- holoviews/plotting/bokeh/links.py | 16 ++++++++-------- holoviews/plotting/plot.py | 13 ++++++------- holoviews/tests/plotting/bokeh/test_callbacks.py | 3 ++- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index aaac154b78..eb78b4989b 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -146,25 +146,25 @@ def __init__(self, root_model, link, source_plot, target_plot): target_range_name = f'subcoordinate_{range_name}' else: target_range_name = range_name - axes[range_name] = target_plot.handles[target_range_name] + axes[range_name] = ax = target_plot.handles[target_range_name] interval = getattr(link, f'intervals{axis}', None) if interval is not None and bokeh34: min, max = interval if min is not None: - axes[range_name].min_interval = min + ax.min_interval = min if max is not None: - axes[range_name].max_interval = max - self._set_range_for_interval(axes[range_name], max) + ax.max_interval = max + self._set_range_for_interval(ax, max) bounds = getattr(link, f'bounds{axis}', None) if bounds is not None: start, end = bounds if start is not None: - axes[range_name].start = start - axes[range_name].reset_start = start + ax.start = start + ax.reset_start = start if end is not None: - axes[range_name].end = end - axes[range_name].reset_end = end + ax.end = end + ax.reset_end = end tool = RangeTool(**axes) source_plot.state.add_tools(tool) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index ab57d3dd4c..7875924b9f 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -980,18 +980,18 @@ def _construct_callbacks(self): callbacks = Stream._callbacks[self.backend] for source in self.link_sources: streams = [] - for src, src_streams in registry: + for stream_src, src_streams in registry: # Skip if source identities do not match - if (src is not source and (src._plot_id is None or src._plot_id != source._plot_id)): + if (stream_src is not source and (stream_src._plot_id is None or stream_src._plot_id != source._plot_id)): continue for stream in src_streams: # Skip if Stream.source is an overlay but the plot isn't # or if the source is an element but the plot isn't src_el = stream.source.last if isinstance(stream.source, HoloMap) else stream.source - if ((isinstance(src_el, CompositeOverlay) and - not isinstance(self, GenericOverlayPlot)) or - (isinstance(src_el, Element) and - isinstance(self, GenericOverlayPlot))): + if ( + (isinstance(src_el, CompositeOverlay) and not (isinstance(self, GenericOverlayPlot) or self.batched)) or + (isinstance(src_el, Element) and isinstance(self, GenericOverlayPlot)) + ): continue streams.append(stream) cb_classes |= {(callbacks[type(stream)], stream) for stream in streams @@ -1005,7 +1005,6 @@ def _construct_callbacks(self): if cb_stream not in source_streams: source_streams.append(cb_stream) cbs.append(cb(self, cb_streams, source)) - print(cbs[-1], type(self)) return cbs, source_streams @property diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py index 193448ffc2..e8fd582e9b 100644 --- a/holoviews/tests/plotting/bokeh/test_callbacks.py +++ b/holoviews/tests/plotting/bokeh/test_callbacks.py @@ -501,7 +501,8 @@ def test_rangexy_subcoordinate_y(): p1, p2 = plot.subplots.values() - assert not (p1.callbacks or p2.callbacks) + assert not p1.callbacks + assert not p2.callbacks assert len(plot.callbacks) == 1 callback = plot.callbacks[0] assert callback._process_msg({}) == {} From ad8a32e70d7948e37203379dc255f320a5c3978f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 6 Jun 2024 16:46:18 +0200 Subject: [PATCH 06/13] Update holoviews/tests/plotting/bokeh/test_callbacks.py --- holoviews/tests/plotting/bokeh/test_callbacks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py index e8fd582e9b..cd80e50ef9 100644 --- a/holoviews/tests/plotting/bokeh/test_callbacks.py +++ b/holoviews/tests/plotting/bokeh/test_callbacks.py @@ -523,7 +523,8 @@ def cb(x_range, y_range): p1, p2 = plot.subplots.values() - assert not (p1.callbacks or p2.callbacks) + assert not p1.callbacks + assert not p2.callbacks assert len(plot.callbacks) == 1 callback = plot.callbacks[0] assert callback._process_msg({}) == {} From c4d9acfdcde3b0a13ea0a69b70d66df4868fb251 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 16:53:57 +0200 Subject: [PATCH 07/13] Fix lint --- holoviews/tests/plotting/bokeh/test_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py index cd80e50ef9..3aedf61145 100644 --- a/holoviews/tests/plotting/bokeh/test_callbacks.py +++ b/holoviews/tests/plotting/bokeh/test_callbacks.py @@ -523,7 +523,7 @@ def cb(x_range, y_range): p1, p2 = plot.subplots.values() - assert not p1.callbacks + assert not p1.callbacks assert not p2.callbacks assert len(plot.callbacks) == 1 callback = plot.callbacks[0] From 6e15425138c8344ba42151ae70835a472ff4f5d8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 17:28:01 +0200 Subject: [PATCH 08/13] Fix source validation of Links --- holoviews/plotting/bokeh/links.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index eb78b4989b..54e1f46d72 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -2,6 +2,9 @@ from bokeh.models import CustomJS, Toolbar from bokeh.models.tools import RangeTool +from ...core.element import Element +from ...core.overlay import CompositeOverlay +from ...core.spaces import HoloMap from ...core.util import isscalar from ..links import ( DataLink, @@ -108,17 +111,25 @@ def find_link(cls, plot, link=None): registry = Link.registry.items() for source in plot.link_sources: if link is None: - links = [ - l for src, links in registry for l in links - if src is source or (src._plot_id is not None and - src._plot_id == source._plot_id)] + candidates = registry + else: + candidates = [(link.source, [link])] + for link_src, src_links in candidates: + if (link_src is not source and (link_src._plot_id is None or link_src._plot_id != source._plot_id)): + continue + links = [] + for link in src_links: + # Skip if Link.source is an overlay but the plot isn't + # or if the source is an element but the plot isn't + src_el = link.source.last if isinstance(link.source, HoloMap) else link.source + if ( + (isinstance(src_el, CompositeOverlay) and not (isinstance(plot, GenericOverlayPlot) or plot.batched)) or + (isinstance(src_el, Element) and isinstance(plot, GenericOverlayPlot)) + ): + continue + links.append(link) if links: return (plot, links) - elif ((link.target is source) or - (link.target is not None and - link.target._plot_id is not None and - link.target._plot_id == source._plot_id)): - return (plot, [link]) def validate(self): """ From 42395f5e977ebf8793db63c5a7f828e63adab8ff Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 17:44:07 +0200 Subject: [PATCH 09/13] Fix logic --- holoviews/plotting/bokeh/links.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 54e1f46d72..5eac68214b 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -108,20 +108,19 @@ def find_link(cls, plot, link=None): """ Searches a GenericElementPlot for a Link. """ - registry = Link.registry.items() + if link is None: + candidates = list(Link.registry.items()) + else: + candidates = [(link.target, [link])] for source in plot.link_sources: - if link is None: - candidates = registry - else: - candidates = [(link.source, [link])] for link_src, src_links in candidates: if (link_src is not source and (link_src._plot_id is None or link_src._plot_id != source._plot_id)): continue links = [] for link in src_links: - # Skip if Link.source is an overlay but the plot isn't - # or if the source is an element but the plot isn't - src_el = link.source.last if isinstance(link.source, HoloMap) else link.source + # Skip if Link.target is an overlay but the plot isn't + # or if the target is an element but the plot isn't + src_el = link.target.last if isinstance(link.target, HoloMap) else link.target if ( (isinstance(src_el, CompositeOverlay) and not (isinstance(plot, GenericOverlayPlot) or plot.batched)) or (isinstance(src_el, Element) and isinstance(plot, GenericOverlayPlot)) From 7619d521e36ac61c72ff77305dd87e944455b036 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 6 Jun 2024 17:49:44 +0200 Subject: [PATCH 10/13] Another fix --- holoviews/plotting/bokeh/links.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 5eac68214b..5b63de6f70 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -97,21 +97,22 @@ def find_links(cls, root_plot): # If link has no target don't look further found.append((link, plot, None)) continue - potentials = [cls.find_link(p, link) for p in plots] + potentials = [cls.find_link(p, link, target=True) for p in plots] tgt_links = [p for p in potentials if p is not None] if tgt_links: found.append((link, plot, tgt_links[0][0])) return found @classmethod - def find_link(cls, plot, link=None): + def find_link(cls, plot, link=None, target=False): """ Searches a GenericElementPlot for a Link. """ + attr = 'target' if target else 'source' if link is None: candidates = list(Link.registry.items()) else: - candidates = [(link.target, [link])] + candidates = [(getattr(link, attr), [link])] for source in plot.link_sources: for link_src, src_links in candidates: if (link_src is not source and (link_src._plot_id is None or link_src._plot_id != source._plot_id)): @@ -120,7 +121,8 @@ def find_link(cls, plot, link=None): for link in src_links: # Skip if Link.target is an overlay but the plot isn't # or if the target is an element but the plot isn't - src_el = link.target.last if isinstance(link.target, HoloMap) else link.target + src = getattr(link, attr) + src_el = src.last if isinstance(src, HoloMap) else src if ( (isinstance(src_el, CompositeOverlay) and not (isinstance(plot, GenericOverlayPlot) or plot.batched)) or (isinstance(src_el, Element) and isinstance(plot, GenericOverlayPlot)) From fd1d180b69e02988f303360ce76c5a35d4df6ebc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 7 Jun 2024 10:25:46 +0200 Subject: [PATCH 11/13] Minor refactoring --- holoviews/plotting/bokeh/links.py | 7 +------ holoviews/plotting/plot.py | 14 ++++++++++---- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 5b63de6f70..512225317e 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -2,8 +2,6 @@ from bokeh.models import CustomJS, Toolbar from bokeh.models.tools import RangeTool -from ...core.element import Element -from ...core.overlay import CompositeOverlay from ...core.spaces import HoloMap from ...core.util import isscalar from ..links import ( @@ -123,10 +121,7 @@ def find_link(cls, plot, link=None, target=False): # or if the target is an element but the plot isn't src = getattr(link, attr) src_el = src.last if isinstance(src, HoloMap) else src - if ( - (isinstance(src_el, CompositeOverlay) and not (isinstance(plot, GenericOverlayPlot) or plot.batched)) or - (isinstance(src_el, Element) and isinstance(plot, GenericOverlayPlot)) - ): + if not plot._matching_plot_type(src_el): continue links.append(link) if links: diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 7875924b9f..662dfa4eab 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -969,6 +969,15 @@ class CallbackPlot: backend = None + def _matching_plot_type(self, element): + """ + Checks if the plot type matches the element type. + """ + return ( + (not isinstance(element, CompositeOverlay) or isinstance(self, GenericOverlayPlot) or self.batched) and + (not isinstance(element, Element) or not isinstance(self, GenericOverlayPlot)) + ) + def _construct_callbacks(self): """ Initializes any callbacks for streams which have defined @@ -988,10 +997,7 @@ def _construct_callbacks(self): # Skip if Stream.source is an overlay but the plot isn't # or if the source is an element but the plot isn't src_el = stream.source.last if isinstance(stream.source, HoloMap) else stream.source - if ( - (isinstance(src_el, CompositeOverlay) and not (isinstance(self, GenericOverlayPlot) or self.batched)) or - (isinstance(src_el, Element) and isinstance(self, GenericOverlayPlot)) - ): + if not self._matching_plot_type(src_el): continue streams.append(stream) cb_classes |= {(callbacks[type(stream)], stream) for stream in streams From ce0cf9e1aa16d39d71e6dc102d828d940698f596 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 7 Jun 2024 10:30:52 +0200 Subject: [PATCH 12/13] More refactoring --- holoviews/plotting/bokeh/links.py | 2 +- holoviews/plotting/plot.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 512225317e..ea168f3cfc 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -113,7 +113,7 @@ def find_link(cls, plot, link=None, target=False): candidates = [(getattr(link, attr), [link])] for source in plot.link_sources: for link_src, src_links in candidates: - if (link_src is not source and (link_src._plot_id is None or link_src._plot_id != source._plot_id)): + if not plot._sources_match(link_src, source): continue links = [] for link in src_links: diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 662dfa4eab..99ddd489d4 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -969,6 +969,10 @@ class CallbackPlot: backend = None + @staticmethod + def _sources_match(src1, src2): + return src1 is src2 or (src1._plot_id is not None and src1._plot_id == src2._plot_id) + def _matching_plot_type(self, element): """ Checks if the plot type matches the element type. @@ -991,7 +995,7 @@ def _construct_callbacks(self): streams = [] for stream_src, src_streams in registry: # Skip if source identities do not match - if (stream_src is not source and (stream_src._plot_id is None or stream_src._plot_id != source._plot_id)): + if not self._sources_match(stream_src, source): continue for stream in src_streams: # Skip if Stream.source is an overlay but the plot isn't From 683c6041b86c811bf3fcb732b8d06a81353ce6ef Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 7 Jun 2024 12:52:45 +0200 Subject: [PATCH 13/13] Add docstring --- holoviews/plotting/bokeh/links.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index ea168f3cfc..f494ca52ab 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -104,7 +104,15 @@ def find_links(cls, root_plot): @classmethod def find_link(cls, plot, link=None, target=False): """ - Searches a GenericElementPlot for a Link. + Searches a plot for any Links declared on the sources of the plot. + + Args: + plot: The plot to search for Links + link: A Link instance to check for matches + target: Whether to check against the Link.target + + Returns: + A tuple containing the matched plot and list of matching Links. """ attr = 'target' if target else 'source' if link is None: