Skip to content

Commit

Permalink
Zoom tools automatically vertically scaled on subcoordinate_y overlays (
Browse files Browse the repository at this point in the history
  • Loading branch information
maximlt authored Jan 15, 2024
1 parent 4672b04 commit 307b1fb
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 5 deletions.
61 changes: 56 additions & 5 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,16 +322,32 @@ def _init_tools(self, element, callbacks=None):
tool = tools.HoverTool(
tooltips=tooltips, tags=['hv_created'], mode=tool, **hover_opts
)
elif bokeh32 and tool in ['wheel_zoom', 'xwheel_zoom', 'ywheel_zoom']:
if tool.startswith('x'):
elif bokeh32 and isinstance(tool, str) and tool.endswith(
('wheel_zoom', 'zoom_in', 'zoom_out')
):
zoom_kwargs = {}
tags = ['hv_created']
if self.subcoordinate_y and not tool.startswith('x'):
zoom_dims = 'height'
zoom_kwargs['level'] = 1
tags.append(tool)
elif tool.startswith('x'):
zoom_dims = 'width'
elif tool.startswith('y'):
zoom_dims = 'height'
else:
zoom_dims = 'both'
tool = tools.WheelZoomTool(
zoom_together='none', dimensions=zoom_dims, tags=['hv_created']
)
zoom_kwargs['dimensions'] = zoom_dims
zoom_kwargs['tags'] = tags
if tool.endswith('wheel_zoom'):
# Setting `zoom_together` for multi-y axis support.
zoom_kwargs['zoom_together'] = 'none'
zoom_type = tools.WheelZoomTool
elif tool.endswith('zoom_in'):
zoom_type = tools.ZoomInTool
elif tool.endswith('zoom_out'):
zoom_type = tools.ZoomOutTool
tool = zoom_type(**zoom_kwargs)
tool_list.append(tool)

copied_tools = []
Expand All @@ -350,6 +366,15 @@ def _init_tools(self, element, callbacks=None):
if hover:
self.handles['hover'] = hover

if self.subcoordinate_y:
zoom_tools = {}
_zoom_types = (tools.WheelZoomTool, tools.ZoomInTool, tools.ZoomOutTool)
for t in copied_tools:
if isinstance(t, _zoom_types) and 'hv_created' in t.tags and len(t.tags) == 2:
zoom_tools[t.tags[1]] = t
if zoom_tools:
self.handles['zooms_subcoordy'] = zoom_tools

box_tools = [t for t in copied_tools if isinstance(t, tools.BoxSelectTool)]
if box_tools:
self.handles['box_select'] = box_tools[0]
Expand Down Expand Up @@ -1828,6 +1853,14 @@ def _init_glyphs(self, plot, element, ranges, source):

self._postprocess_hover(renderer, source)

zooms_subcoordy = self.handles.get('zooms_subcoordy')
if zooms_subcoordy is not None:
for zoom in zooms_subcoordy.values():
# The default renderer is 'auto', instead we want to
# store the subplot renderer to aggregate them and set
# the final tool with a list of all the renderers.
zoom.renderers = [renderer]

# Update plot, source and glyph
with abbreviated_exception():
self._update_glyph(renderer, properties, mapping, glyph, source, source.data)
Expand Down Expand Up @@ -2777,6 +2810,8 @@ def _init_tools(self, element, callbacks=None):
if callbacks is None:
callbacks = []
hover_tools = {}
zooms_subcoordy = {}
_zoom_types = (tools.WheelZoomTool, tools.ZoomInTool, tools.ZoomOutTool)
init_tools, tool_types = [], []
for key, subplot in self.subplots.items():
el = element.get(key)
Expand All @@ -2793,6 +2828,15 @@ def _init_tools(self, element, callbacks=None):
continue
else:
hover_tools[tooltips] = tool
elif (
self.subcoordinate_y and isinstance(tool, _zoom_types)
and 'hv_created' in tool.tags and len(tool.tags) == 2
):
if tool.tags[1] in zooms_subcoordy:
continue
else:
zooms_subcoordy[tool.tags[1]] = tool
self.handles['zooms_subcoordy'] = zooms_subcoordy
elif tool_type in tool_types:
continue
else:
Expand Down Expand Up @@ -2822,6 +2866,13 @@ def _merge_tools(self, subplot):
tool.renderers = list(util.unique_iterator(renderers))
if 'hover' not in self.handles:
self.handles['hover'] = tool
if 'zooms_subcoordy' in subplot.handles and 'zooms_subcoordy' in self.handles:
for subplot_zoom, overlay_zoom in zip(
subplot.handles['zooms_subcoordy'].values(),
self.handles['zooms_subcoordy'].values(),
):
renderers = list(util.unique_iterator(subplot_zoom.renderers + overlay_zoom.renderers))
overlay_zoom.renderers = renderers

def _get_dimension_factors(self, overlay, ranges, dimension):
factors = []
Expand Down
48 changes: 48 additions & 0 deletions holoviews/tests/plotting/bokeh/test_subcoordy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numpy as np
import pytest
from bokeh.models.tools import WheelZoomTool, ZoomInTool, ZoomOutTool

from holoviews.core import Overlay
from holoviews.element import Curve
Expand Down Expand Up @@ -202,3 +203,50 @@ def test_same_label_error(self):
match='Elements wrapped in a subcoordinate_y overlay must all have a unique label',
):
bokeh_renderer.get_plot(overlay)

def test_tools_default_wheel_zoom_configured(self):
overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True) for i in range(2)])
plot = bokeh_renderer.get_plot(overlay)
zoom_subcoordy = plot.handles['zooms_subcoordy']['wheel_zoom']
assert len(zoom_subcoordy.renderers) == 2
assert len(set(zoom_subcoordy.renderers)) == 2
assert zoom_subcoordy.dimensions == 'height'
assert zoom_subcoordy.level == 1

def test_tools_string_zoom_in_out_configured(self):
for zoom in ['zoom_in', 'zoom_out', 'yzoom_in', 'yzoom_out', 'ywheel_zoom']:
overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True, tools=[zoom]) for i in range(2)])
plot = bokeh_renderer.get_plot(overlay)
zoom_subcoordy = plot.handles['zooms_subcoordy'][zoom]
assert len(zoom_subcoordy.renderers) == 2
assert len(set(zoom_subcoordy.renderers)) == 2
assert zoom_subcoordy.dimensions == 'height'
assert zoom_subcoordy.level == 1

def test_tools_string_x_zoom_untouched(self):
for zoom, zoom_type in [
('xzoom_in', ZoomInTool),
('xzoom_out', ZoomOutTool),
('xwheel_zoom', WheelZoomTool),
]:
overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True, tools=[zoom]) for i in range(2)])
plot = bokeh_renderer.get_plot(overlay)
for tool in plot.state.tools:
if isinstance(tool, zoom_type) and tool.tags == ['hv_created']:
assert tool.level == 0
assert tool.dimensions == 'width'
break
else:
raise AssertionError('Provided zoom not found.')

def test_tools_instance_zoom_untouched(self):
for zoom in [WheelZoomTool(), ZoomInTool(), ZoomOutTool()]:
overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True, tools=[zoom]) for i in range(2)])
plot = bokeh_renderer.get_plot(overlay)
for tool in plot.state.tools:
if isinstance(tool, type(zoom)) and 'hv_created' not in tool.tags:
assert tool.level == 0
assert tool.dimensions == 'both'
break
else:
raise AssertionError('Provided zoom not found.')

0 comments on commit 307b1fb

Please sign in to comment.