-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
[FEATURE] Create option for wheel zoom tool to apply only to subplot nearest to the cursor position #13728
Comments
That captures it all well, thanks! My concern is that the current status quo is not intuitive or discoverable or easy to use; if we have multiple zoom tools in the toolbar, which group they apply to is obscure, the fact that you have to pick one or the other is something the user has to figure out fairly painfully, and switching between groups is awkward. Whereas pointing to a group and using the scroll wheel is intuitive and obvious and doesn't require explanation; people will discover it immediately ("Oh! It only zooms this group! What if I move to the other group; does it now zoom that? Yes! Perfect!"). I strongly prefer UIs that don't require explanation. :-) |
To expand on this, the implementation in #13826 currently implements the ability to enable the contextual hit-testing behavior. However I don't think it quite matches the behavior defined here, specifically the behavior requested here involves a situation where:
As far as I can tell #13826 implements slightly different behavior:
Both are useful behaviors but they are quite different and currently #13826 does not cover the use case we were imagining. We would need a separate |
@philippjfr, @mattpap I think the "situation" above is actually describing part of the problem that I was trying (but clearly failing) to initially describe. My apologies about that. We want to avoid requiring multiple zoom tools (one per renderer group). I don't think the updated implementation in #13826 is sufficient yet to avoid this problem, or maybe I'm misunderstanding it. Hopefully this helps to illustrate: In the example below (which uses the #13826 branch), there are two renderer groups and a zoom tool per group. We simply want to avoid having two separate zoom tools in such an example. The intent of the original request was to have a single zoom tool apply to a specific renderer group depending on whether a member of that group was hit, without having to switch tools in the toolbar. Codefrom bokeh.layouts import column
from bokeh.plotting import figure, curdoc
import numpy as np
from bokeh.core.properties import field
from bokeh.layouts import column, row
from bokeh.models import (ColumnDataSource, CustomJS, Div, FactorRange, HoverTool,
Range1d, Switch, WheelZoomTool, ZoomInTool, ZoomOutTool)
from bokeh.palettes import Category10
from bokeh.plotting import figure
n_eeg_channels = 7
n_pos_channels = 3
n_channels = n_eeg_channels + n_pos_channels
n_seconds = 15
total_samples = 512*n_seconds
time = np.linspace(0, n_seconds, total_samples)
data = np.random.randn(n_channels, total_samples).cumsum(axis=1)
channels = [f"EEG {i}" for i in range(n_eeg_channels)] + [f"POS {i}" for i in range(n_pos_channels)]
hover = HoverTool(tooltips=[
("Channel", "$name"),
("Time", "$x s"),
("Amplitude", "$y μV"),
])
x_range = Range1d(start=time.min(), end=time.max())
y_range = FactorRange(factors=channels)
p = figure(x_range=x_range, y_range=y_range, lod_threshold=None, tools="pan,reset,xcrosshair")
source = ColumnDataSource(data=dict(time=time))
eeg_renderers = []
pos_renderers = []
for i, channel in enumerate(channels):
is_eeg = channel.startswith('EEG')
xy = p.subplot(
x_source=p.x_range,
y_source=Range1d(start=data[i].min(), end=data[i].max()),
x_target=p.x_range,
y_target=Range1d(start=i, end=i + 1),
)
source.data[channel] = data[i]
if is_eeg:
line = xy.line(field("time"), field(channel), color='black', source=source, name=channel)
eeg_renderers.append(line)
else:
line = xy.line(field("time"), field(channel), color=Category10[10][i], source=source, name=channel)
pos_renderers.append(line)
all_renderers = eeg_renderers + pos_renderers
level = 1
hit_test = True
pos_ywheel_zoom = WheelZoomTool(renderers=eeg_renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior="all", dimensions="height")
eeg_ywheel_zoom = WheelZoomTool(renderers=pos_renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior="all", dimensions="height")
xwheel_zoom = WheelZoomTool(renderers=all_renderers, level=level, dimensions="width")
zoom_in = ZoomInTool(renderers=all_renderers, level=level, dimensions="height")
zoom_out = ZoomOutTool(renderers=all_renderers, level=level, dimensions="height")
p.add_tools(pos_ywheel_zoom, eeg_ywheel_zoom, xwheel_zoom, zoom_in, zoom_out, hover)
p.toolbar.active_scroll = eeg_ywheel_zoom
level_switch = Switch(active=level == 1)
level_switch.js_on_change("active", CustomJS(
args=dict(tools=[eeg_ywheel_zoom, pos_ywheel_zoom, zoom_in, zoom_out]),
code="""
export default ({tools}, obj) => {
const level = obj.active ? 1 : 0
for (const tool of tools) {
tool.level = level
}
}
"""))
hit_test_switch = Switch(active=hit_test)
hit_test_switch.js_on_change("active", CustomJS(
args=dict(tool=eeg_ywheel_zoom),
code="""
export default ({tool}, obj) => {
tool.hit_test = obj.active
}
"""))
layout = column(
row(Div(text="Zoom sub-coordinates:"), level_switch),
row(Div(text="Zoom hit-tested:"), hit_test_switch),
p,
)
curdoc().add_root(layout) video1791141250.mp4 |
Problem description
In the current implementation, when dealing with visualizations that incorporate multiple subplots (e.g., for simultaneously recorded timeseries data such as EEG, MEG, ECG in neuroscience), there's a need to zoom into specific groups of timeseries without affecting others. However, the current toolset necessitates creating multiple zoom tools for each group, leading to a cluttered interface. With the number of zoom tools scaling with the number of source groups, this becomes impractical beyond a couple of groups, compromising usability.
Feature description
I propose an enhancement to the wheel zoom tool that allows it to dynamically target the subplot (or group of subplots) under the cursor for zooming actions. This feature would enable a single wheel zoom tool to apply selectively based on the cursor's position over the plot, streamlining user interactions by keeping attention in the viewport rather than on an array of similar looking tools in the toolbar.
One potential implementation would be to expand the options for the 'level' arg (which normally specifies whether a tool applies to a base plot (
level = 0
) or a subplot (level = 1
), see Bokeh's example) to allow for something likelevel='cursor'
. I don't think is the solution since there may be elements on the base/parent level. Probably a better solution would be to utilize a new arg, something likefollow_cursor=True
, which, when combined withlevel=1
, would allow for the behavior being described - only the subplot closest to the cursor is impacted. Yet another solution would be to create a distinct zoom tool specifically for this behavior.This approach would simplify the UI and make it more intuitive for users to zoom into specific parts of the visualization relevant to their point of interest.
Potential alternatives
One alternative considered is maintaining the status quo with multiple zoom tools for each group, which is not scalable. A complex solution could involve custom scripting to manage zoom levels and targets dynamically, but this would significantly increase the burden on the user.
One less good but possible alternative is to utilize a dropdown menu in the toolbar to select a particular subplot group and then have a zoom tool apply to the selected group. While this mitigates the scalability issue, it adds complexity to specifying this custom dropdown tool for an action that should be easier to express and apply.
Additional information
Anticipated challenges:
Handling scenarios where subplots overlap, which might necessitate a decision on which subplot(s) the zoom applies to when the cursor is above multiple groups. An initial approach could be to apply the zoom action to all underlying subplots, with further refinements as needed.
When zooming out, at some point the subplot under the cursor may not be under the cursor any more, so 'applying to nearest subplot' is probably needed to maintain consistency.
Integrating this dynamic zoom functionality with tap zoom tools presents a challenge, as tap zoom requires toolbar interaction. I suggest focusing on wheel zoom. Tap zoom might require a separate mechanism, such as an 'active group' selection through a dropdown in the toolbar... so separate issue.
tagging @jbednar, from whom this request most recently came.
The text was updated successfully, but these errors were encountered: