Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/14040.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make the vertical (channel) and horizontal (time) scrollbar handles in the ``'matplotlib'`` browser backend draggable, by `Clemens Brunner`_.
59 changes: 57 additions & 2 deletions mne/viz/_mpl_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ def _buttonpress(self, event):
"""Handle buttonpress events."""
pass

def _buttonrelease(self, event):
"""Handle button release events."""
pass

def _mouse_move(self, event):
"""Handle mouse motion events."""
pass

def _scroll(self, event):
"""Handle scroll wheel events."""
pass
Expand All @@ -245,6 +253,8 @@ def _add_default_callbacks(self, **kwargs):
resize_event=self._resize,
key_press_event=self._keypress,
button_press_event=self._buttonpress,
button_release_event=self._buttonrelease,
motion_notify_event=self._mouse_move,
scroll_event=self._scroll,
close_event=self._close,
pick_event=self._pick,
Expand Down Expand Up @@ -661,6 +671,8 @@ def __init__(self, inst, figsize, ica=None, xlabel="Time (s)", **kwargs):
ax_vscroll=ax_vscroll,
vsel_patch=vsel_patch,
hsel_patch=hsel_patch,
vscroll_drag_offset=None,
hscroll_drag_offset=None,
vline=vline,
vline_hscroll=vline_hscroll,
vline_text=vline_text,
Expand Down Expand Up @@ -924,12 +936,15 @@ def _buttonpress(self, event):
elif event.inaxes == self.mne.ax_vscroll:
if self.mne.fig_selection is not None:
self._change_selection_vscroll(event)
elif self._check_update_vscroll_clicked(event):
self._redraw()
else:
if self._check_update_vscroll_clicked(event):
self._redraw()
self.mne.vscroll_drag_offset = event.ydata - self.mne.ch_start
# click in horizontal scrollbar
elif event.inaxes == self.mne.ax_hscroll:
if self._check_update_hscroll_clicked(event):
self._redraw(annotations=True)
self.mne.hscroll_drag_offset = event.xdata - self.mne.t_start
# click on proj button
elif event.inaxes == self.mne.ax_proj:
self._toggle_proj_fig(event)
Expand Down Expand Up @@ -971,6 +986,46 @@ def _buttonpress(self, event):
elif event.inaxes == ax_main:
self._toggle_vline(False)

def _buttonrelease(self, event):
"""Handle mouse button releases (end scrollbar handle drags)."""
self.mne.vscroll_drag_offset = None
self.mne.hscroll_drag_offset = None

def _mouse_move(self, event):
"""Handle mouse motion (drag the scrollbar handles)."""
if self.mne.vscroll_drag_offset is not None:
if event.y is None:
return
ydata = self.mne.ax_vscroll.transData.inverted().transform((0, event.y))[1]
new_ch_start = np.clip(
int(round(ydata - self.mne.vscroll_drag_offset)),
0,
len(self.mne.ch_order) - self.mne.n_channels,
)
if self.mne.ch_start != new_ch_start:
self.mne.ch_start = new_ch_start
self._update_picks()
self._update_vscroll()
self._redraw()
elif self.mne.hscroll_drag_offset is not None:
if event.x is None:
return
xdata = self.mne.ax_hscroll.transData.inverted().transform((event.x, 0))[0]
time = xdata - self.mne.hscroll_drag_offset
max_time = (
self.mne.n_times / self.mne.info["sfreq"]
+ self.mne.first_time
- self.mne.duration
)
time = np.clip(time, self.mne.first_time, max_time)
if self.mne.is_epochs:
ix = np.searchsorted(self.mne.boundary_times[1:], time, side="right")
time = self.mne.boundary_times[ix]
if self.mne.t_start != time:
self.mne.t_start = time
self._update_hscroll()
self._redraw(annotations=True)

def _scroll(self, event):
"""Handle scroll wheel events for channel navigation."""
if self.mne.butterfly or self.mne.fig_selection is not None:
Expand Down
36 changes: 32 additions & 4 deletions mne/viz/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,15 +687,43 @@ def test_plot_raw_traces(raw, events, browser_backend):
fig._fake_click((0.5, 0.05), ax=vscroll) # change channels to end
labels = fig._get_ticklabels("y")
assert labels == [raw.ch_names[5], raw.ch_names[2], raw.ch_names[3]]
for _ in (0, 0):
# first click changes channels to mid; second time shouldn't change
# This needs to be changed for Qt, because there scrollbars are
# drawn differently (value of slider at lower end, not at middle)
for _ in range(2): # first click jumps to mid, second is a no-op (already there)
# mpl centers the handle on the click; Qt's QScrollBar positions the handle at
# its low end, hence the different target for Qt
yclick = 0.5 if ismpl else 0.7
fig._fake_click((0.5, yclick), ax=vscroll)
labels = fig._get_ticklabels("y")
assert labels == [raw.ch_names[7], raw.ch_names[5], raw.ch_names[2]]

# Qt scrollbars are native QScrollBar widgets, so dragging them is already handled
# by Qt itself; here we only need to test the custom drag handling added for the
# 'matplotlib' scrollbars.
if ismpl:
# dragging the vertical scrollbar handle
n_channels = fig.mne.n_channels
ch_start = fig.mne.ch_start
center = ch_start + n_channels / 2
fig._fake_click((0.5, center), ax=vscroll, xform="data", kind="press")
fig._fake_click((0.5, center + 1), ax=vscroll, xform="data", kind="motion")
assert fig.mne.ch_start == ch_start + 1
fig._fake_click((0.5, center + 1), ax=vscroll, xform="data", kind="release")
# further motion after release should be a no-op
fig._fake_click((0.5, center + 5), ax=vscroll, xform="data", kind="motion")
assert fig.mne.ch_start == ch_start + 1

# dragging the horizontal scrollbar handle
duration = fig.mne.duration
t_start = fig.mne.t_start
center = t_start + duration / 2
fig._fake_click((center, 0.5), ax=hscroll, xform="data", kind="press")
fig._fake_click((center + 1, 0.5), ax=hscroll, xform="data", kind="motion")
assert fig.mne.t_start == pytest.approx(t_start + 1, abs=0.05)
fig._fake_click((center + 1, 0.5), ax=hscroll, xform="data", kind="release")
dragged_t_start = fig.mne.t_start
# further motion after release should be a no-op
fig._fake_click((center + 5, 0.5), ax=hscroll, xform="data", kind="motion")
assert fig.mne.t_start == dragged_t_start

# test clicking a channel name in butterfly mode
bads = fig.mne.info["bads"].copy()
fig._fake_keypress("b")
Expand Down
Loading