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
8 changes: 6 additions & 2 deletions mne/viz/_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,13 +1516,17 @@ def _plot_mpl_stc(stc, subject=None, surface='inflated', hemi='lh',
return fig


def link_brains(brains):
def link_brains(brains, time=True, camera=False):
"""Plot multiple SourceEstimate objects with PyVista.

Parameters
----------
brains : list, tuple or np.ndarray
The collection of brains to plot.
time : bool
If True, link the time controllers. Defaults to True.
camera : bool
If True, link the camera controls. Defaults to False.
"""
from .backends.renderer import _get_3d_backend
if _get_3d_backend() != 'pyvista':
Expand All @@ -1542,7 +1546,7 @@ def link_brains(brains):
raise TypeError("Expected type is Brain but"
" {} was given.".format(type(brain)))
# link brains properties
_LinkViewer(brains)
_LinkViewer(brains, time, camera)


@verbose
Expand Down
75 changes: 55 additions & 20 deletions mne/viz/_brain/_timeviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def __init__(self, time_viewer, width, height, dpi):
# XXX eventually this should be called in the window resize callback
tight_layout(fig=self.axes.figure)
self.time_viewer = time_viewer
self.time_func = time_viewer.time_call
for event in ('button_press', 'motion_notify'):
self.canvas.mpl_connect(
event + '_event', getattr(self, 'on_' + event))
Expand Down Expand Up @@ -117,7 +118,7 @@ def on_button_press(self, event):
if (event.inaxes != self.axes or
event.button != 1):
return
self.time_viewer.time_call(
self.time_func(
event.xdata, update_widget=True, time_as_index=False)

on_motion_notify = on_button_press # for now they can be the same
Expand Down Expand Up @@ -1199,28 +1200,48 @@ def clean(self):
class _LinkViewer(object):
"""Class to link multiple _TimeViewer objects."""

def __init__(self, brains):
def __init__(self, brains, time=True, camera=False):
self.brains = brains
self.time_viewers = [brain.time_viewer for brain in brains]

# link time sliders
self.link_sliders(
name="_time_slider",
callback=self.set_time_point,
event_type="always"
)
# check time infos
times = [brain._times for brain in brains]
if time and not all(np.allclose(x, times[0]) for x in times):
warn('stc.times do not match, not linking time')
time = False

if camera:
self.link_cameras()

if time:
# link time sliders
self.link_sliders(
name="_time_slider",
callback=self.set_time_point,
event_type="always"
)

# link playback speed sliders
self.link_sliders(
name="_playback_speed_slider",
callback=self.set_playback_speed,
event_type="always"
)
# link playback speed sliders
self.link_sliders(
name="_playback_speed_slider",
callback=self.set_playback_speed,
event_type="always"
)

# link toggle to start/pause playback
for time_viewer in self.time_viewers:
time_viewer.actions["play"].triggered.disconnect()
time_viewer.actions["play"].triggered.connect(self.toggle_playback)
# link toggle to start/pause playback
for time_viewer in self.time_viewers:
time_viewer.actions["play"].triggered.disconnect()
time_viewer.actions["play"].triggered.connect(
self.toggle_playback)

# link time course canvas
def _func(*args, **kwargs):
for time_viewer in self.time_viewers:
time_viewer.time_call(*args, **kwargs)

for time_viewer in self.time_viewers:
if time_viewer.show_traces:
time_viewer.mpl_canvas.time_func = _func

def set_time_point(self, value):
for time_viewer in self.time_viewers:
Expand All @@ -1231,8 +1252,8 @@ def set_playback_speed(self, value):
time_viewer.playback_speed_call(value, update_widget=True)

def toggle_playback(self):
master = self.time_viewers[0] # select a master time_viewer
value = master.time_call.slider_rep.GetValue()
leader = self.time_viewers[0] # select a time_viewer as leader
value = leader.time_call.slider_rep.GetValue()
# synchronize starting points before playback
self.set_time_point(value)
for time_viewer in self.time_viewers:
Expand All @@ -1249,6 +1270,20 @@ def link_sliders(self, name, callback, event_type):
event_type=event_type
)

def link_cameras(self):
from ..backends._pyvista import _add_camera_callback

def _update_camera(vtk_picker, event):
for time_viewer in self.time_viewers:
time_viewer.plotter.update()

leader = self.time_viewers[0] # select a time_viewer as leader
camera = leader.plotter.camera
_add_camera_callback(camera, _update_camera)
for time_viewer in self.time_viewers:
for renderer in time_viewer.plotter.renderers:
renderer.camera = camera


def _get_range(brain):
val = np.abs(brain._current_act_data)
Expand Down
6 changes: 5 additions & 1 deletion mne/viz/_brain/tests/test_brain.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,11 @@ def test_brain_linkviewer(renderer_interactive, travis_macos):
brain_data = _create_testing_brain(hemi='split')
_TimeViewer(brain_data)

link_viewer = _LinkViewer([brain_data])
link_viewer = _LinkViewer(
[brain_data],
time=True,
camera=True,
)
link_viewer.set_time_point(value=0)
link_viewer.set_playback_speed(value=0.1)
link_viewer.toggle_playback()
Expand Down
4 changes: 4 additions & 0 deletions mne/viz/backends/_pyvista.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,10 @@ def _the_callback(widget, event):
slider.AddObserver(event, _the_callback)


def _add_camera_callback(camera, callback):
camera.AddObserver(vtk.vtkCommand.ModifiedEvent, callback)


def _update_picking_callback(plotter,
on_mouse_move,
on_button_press,
Expand Down
2 changes: 1 addition & 1 deletion mne/viz/tests/test_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ def test_link_brains(renderer_interactive):
link_brains([])
with pytest.raises(TypeError, match='type is Brain'):
link_brains('foo')
link_brains(brain)
link_brains(brain, time=True, camera=True)


def test_renderer(renderer):
Expand Down