-
-
Notifications
You must be signed in to change notification settings - Fork 403
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
Apply subcoordinate zoom by group #5901
Comments
I assume this means users would set the
Here's a simple pure Bokeh approach to that, registering Zoom In/Out tools for each group with their own description. The same could be done for the wheel zoom tool. Code
import numpy as np
from bokeh.core.properties import field
from bokeh.io import show
from bokeh.models import (ColumnDataSource, FactorRange,
HoverTool, Range1d, ZoomInTool, ZoomOutTool)
from bokeh.plotting import figure
from bokeh.palettes import Category10
np.random.seed(0)
n_channels = 10
n_seconds = 15
fs = 512
max_ch_disp = 5 # max channels to initially display
max_t_disp = 3 # max time in seconds to initially display
total_samples = n_seconds * fs
time = np.linspace(0, n_seconds, total_samples)
data = np.random.randn(n_channels, total_samples).cumsum(axis=1)
group1 = [f'GROUP1 - {i}' for i in range(n_channels - 5)]
group2 = [f'GROUP2 - {i}' for i in range(5, n_channels)]
channels = group1 + group2
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)
source = ColumnDataSource(data=dict(time=time))
from collections import defaultdict
renderers = defaultdict(list)
for i, channel in enumerate(channels):
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]
line = xy.line(field("time"), field(channel), color=Category10[10][i], source=source, name=channel)
renderers[channel.split('-')[0].strip()].append(line)
zoom_in_grp1 = ZoomInTool(renderers=renderers['GROUP1'], level=1, dimensions='height', description='Zoom in (GROUP1)')
zoom_in_grp2 = ZoomInTool(renderers=renderers['GROUP2'], level=1, dimensions='height', description='Zoom in (GROUP`2)')
zoom_out_grp1 = ZoomOutTool(renderers=renderers['GROUP1'], level=1, dimensions='height', description='Zoom out (GROUP1)')
zoom_out_grp2 = ZoomOutTool(renderers=renderers['GROUP2'], level=1, dimensions='height', description='Zoom out (GROUP2)')
p.add_tools(zoom_in_grp1, zoom_out_grp1, zoom_in_grp2, zoom_out_grp2)
show(p) @droumis Would this approach be fine for a first implementation of this feature? If not, or if we wish to refine it later, here are some comments on the suggested implementations (granting that I don't know much about Bokeh tools):
I don't think that a wheel zoom tool can be limited to execute its action only if the cursor is in a specific portion of the frame?
Just to get a brief feeling of how that might work, here's a pure Bokeh implementation that emulates the group selection via a widget and updates the renderers attached to the Zoom In/Out tools and their description. I haven't looked into how line selection could be driving this. Details
import numpy as np
from bokeh.core.properties import field
from bokeh.io import show
from bokeh.models import (ColumnDataSource, FactorRange,
HoverTool, Range1d, ZoomInTool, ZoomOutTool)
from bokeh.plotting import figure
from bokeh.palettes import Category10
np.random.seed(0)
n_channels = 10
n_seconds = 15
fs = 512
max_ch_disp = 5 # max channels to initially display
max_t_disp = 3 # max time in seconds to initially display
total_samples = n_seconds * fs
time = np.linspace(0, n_seconds, total_samples)
data = np.random.randn(n_channels, total_samples).cumsum(axis=1)
group1 = [f'GROUP1 - {i}' for i in range(n_channels - 5)]
group2 = [f'GROUP2 - {i}' for i in range(5, n_channels)]
channels = group1 + group2
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)
source = ColumnDataSource(data=dict(time=time))
from collections import defaultdict
renderers = defaultdict(list)
for i, channel in enumerate(channels):
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]
line = xy.line(field("time"), field(channel), color=Category10[10][i], source=source, name=channel)
renderers[channel.split('-')[0].strip()].append(line)
zoom_in_grp = ZoomInTool(renderers=renderers['GROUP1'], level=1, dimensions='height', description="GROUP1")
zoom_out_grp = ZoomOutTool(renderers=renderers['GROUP1'], level=1, dimensions='height', description="GROUP1")
from bokeh.layouts import column
from bokeh.models import CustomJS, Select
select = Select(title="Group:", value="GROUP1", options=["GROUP1", "GROUP2"])
select.js_on_change("value", CustomJS(args=dict(zoomin=zoom_in_grp, zoomout=zoom_out_grp, renderers=renderers), code="""
zoomin.renderers = renderers[this.value]
zoomin.description = 'Zoom In (' + this.value + ')'
zoomout.renderers = renderers[this.value]
zoomout.description = 'Zoom In (' + this.value + ')'
"""))
p.add_tools(zoom_in_grp, zoom_out_grp)
show(column(select, p)) |
Nice examples, @maximlt! Yes, we can work around the wheel zoom limitations with the discrete approach. Regarding which discrete approach: The toolbar approach is cleaner and could be automatic - but it isn't very scalable to keep adding two toolbar tools per group and also to have those icons identical (even if the description is different). We could potentially nest the various discrete zoom tools, but I don't think that really resolves the issue, it just hides it. Using a select widget approach as you demo'd is more scalable for many groups, but would it likely require more lines of code to produce for users and they would have to configure widget location. Maybe this is niche enough that requiring that users add a select widget to utilize group-specific zoom is fine, with sufficient documentation. Also, if the widget indicated the 'active group' broadly, then people could also use it to potentially trigger/focus other actions/plots. I'm leaning toward this approach for now, especially if it could involve the Panel version of the Select widget. |
@mattpap is it in scope of your toolbar customization PR to support one custom tool (e.g. select) impacting the renderers of another tool (e.g. zoom), similar to what Maxime is showing in the gif above with the select widget and zoom tools, but all in the toolbar? |
@droumis, yes, that's a possibility I considered in general (not specifically related to zoom) when working on that PR. |
@maximlt, another consideration is to ensure that y-axis range scaling remains groupwise for the HoloViews implementation of subcoordinate_y (I don't think that's true for the pure Bokeh versions); the timeseries within a group should all have the same scaling factor, but it can be different for different groups. |
We discussed today and clarified (at least for me 🙃 ) that we'd like to normalize the groups independently by default, potentially offering an option to disable that. |
This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Often, researchers will display a
subcoordinate_y
-like plot with timeseries belonging to different groups (e.g. EEG, MEG, ECG, etc). In this context, each group likely has a different unit, sampling rate, and typical amplitude range.This feature request is to make
subcoordinate_y
group-aware, allowing users to specify to which group each timeseries belongs, and have a subplot-configured zoom tool act only on that group.Potential implementations:
The text was updated successfully, but these errors were encountered: